diff --git a/sdk/core/Azure.Core/src/Shared/ConnectionString.cs b/sdk/core/Azure.Core/src/Shared/ConnectionString.cs index 6c67294d2c05..fcb3c31dbd8b 100644 --- a/sdk/core/Azure.Core/src/Shared/ConnectionString.cs +++ b/sdk/core/Azure.Core/src/Shared/ConnectionString.cs @@ -21,6 +21,9 @@ internal sealed class ConnectionString return new ConnectionString(ParseSegments(connectionString, segmentSeparator, keywordValueSeparator), segmentSeparator, keywordValueSeparator); } + public static ConnectionString Empty(string segmentSeparator = ";", string keywordValueSeparator = "=") => + new ConnectionString(new Dictionary(), segmentSeparator, keywordValueSeparator); + private ConnectionString(Dictionary pairs, string pairSeparator, string keywordValueSeparator) { _pairs = pairs; @@ -34,6 +37,12 @@ public string GetRequired(string keyword) => public string? GetNonRequired(string keyword) => _pairs.TryGetValue(keyword, out var value) ? value : null; + public bool TryGetSegmentValue(string keyword, out string value) => + _pairs.TryGetValue(keyword, out value); + + public bool ContainsSegmentKey(string keyword) => + _pairs.ContainsKey(keyword); + public void Replace(string keyword, string value) { if (_pairs.ContainsKey(keyword)) @@ -42,8 +51,16 @@ public void Replace(string keyword, string value) } } + public void Add(string keyword, string value) => + _pairs.Add(keyword, value); + public override string ToString() { + if (_pairs.Count == 0) + { + return string.Empty; + } + var stringBuilder = new StringBuilder(); var isFirst = true; foreach (KeyValuePair pair in _pairs) diff --git a/sdk/core/Azure.Core/tests/ConnectionStringTests.cs b/sdk/core/Azure.Core/tests/ConnectionStringTests.cs index a948fa5df90b..9df7c092e2ce 100644 --- a/sdk/core/Azure.Core/tests/ConnectionStringTests.cs +++ b/sdk/core/Azure.Core/tests/ConnectionStringTests.cs @@ -61,7 +61,27 @@ public void RequiredKey() { var connectionString = ConnectionString.Parse("x=y"); Assert.AreEqual("y", connectionString.GetRequired("x")); - Assert.Throws(() => connectionString.GetRequired("y")); + Assert.Throws(() => connectionString.GetRequired("notpresent")); + } + + [Test] + public void TryGetSegmentValue() + { + var connectionString = ConnectionString.Parse("x=y"); + Assert.That(connectionString.TryGetSegmentValue("x", out var value)); + Assert.That(value, Is.EqualTo("y")); + Assert.That(connectionString.TryGetSegmentValue("notpresent", out _), Is.False); + } + + [Test] + public void Add() + { + var connectionString = ConnectionString.Parse("x=y"); + Assert.AreEqual("y", connectionString.GetRequired("x")); + Assert.Throws(() => connectionString.GetRequired("notpresent")); + + connectionString.Add("notpresent", "someValue"); + Assert.DoesNotThrow(() => connectionString.GetRequired("notpresent")); } [Test] @@ -82,5 +102,13 @@ public void Replace() Assert.AreEqual("z", connectionString.GetNonRequired("x")); Assert.AreEqual(null, connectionString.GetNonRequired("y")); } + + [Test] + public void EmtptyProducesEmptyString() + { + var connectionSring = ConnectionString.Empty(); + + Assert.That(connectionSring.ToString(), Is.EqualTo(string.Empty)); + } } } diff --git a/sdk/tables/Azure.Data.Tables/CHANGELOG.md b/sdk/tables/Azure.Data.Tables/CHANGELOG.md index 320d347eb4f6..02434784717d 100644 --- a/sdk/tables/Azure.Data.Tables/CHANGELOG.md +++ b/sdk/tables/Azure.Data.Tables/CHANGELOG.md @@ -1,3 +1,3 @@ # Release History -## 1.0.0-preview.1 (Unreleased) +## 3.0.0-beta.1 (Unreleased) diff --git a/sdk/tables/Azure.Data.Tables/api/Azure.Data.Tables.netstandard2.0.cs b/sdk/tables/Azure.Data.Tables/api/Azure.Data.Tables.netstandard2.0.cs index 7b72fe2e828a..67d31bb44c45 100644 --- a/sdk/tables/Azure.Data.Tables/api/Azure.Data.Tables.netstandard2.0.cs +++ b/sdk/tables/Azure.Data.Tables/api/Azure.Data.Tables.netstandard2.0.cs @@ -10,9 +10,11 @@ public partial interface ITableEntity public partial class TableClient { protected TableClient() { } - public TableClient(string tableName, System.Uri endpoint, Azure.Data.Tables.TableClientOptions options = null) { } - public TableClient(string tableName, System.Uri endpoint, Azure.Data.Tables.TableSharedKeyCredential credential) { } - public TableClient(string tableName, System.Uri endpoint, Azure.Data.Tables.TableSharedKeyCredential credential, Azure.Data.Tables.TableClientOptions options = null) { } + public TableClient(string connectionString, string tableName) { } + public TableClient(string connectionString, string tableName, Azure.Data.Tables.TableClientOptions options = null) { } + public TableClient(System.Uri endpoint, string tableName, Azure.Data.Tables.TableClientOptions options = null) { } + public TableClient(System.Uri endpoint, string tableName, Azure.Data.Tables.TableSharedKeyCredential credential) { } + public TableClient(System.Uri endpoint, string tableName, Azure.Data.Tables.TableSharedKeyCredential credential, Azure.Data.Tables.TableClientOptions options = null) { } public virtual System.Threading.Tasks.Task AddEntityAsync(T entity, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) where T : class, Azure.Data.Tables.ITableEntity, new() { throw null; } public virtual Azure.Response AddEntity(T entity, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) where T : class, Azure.Data.Tables.ITableEntity, new() { throw null; } public virtual Azure.Response Create(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } @@ -86,6 +88,8 @@ public void Clear() { } public partial class TableServiceClient { protected TableServiceClient() { } + public TableServiceClient(string connectionString) { } + public TableServiceClient(string connectionString, Azure.Data.Tables.TableClientOptions options = null) { } public TableServiceClient(System.Uri endpoint) { } public TableServiceClient(System.Uri endpoint, Azure.Data.Tables.TableClientOptions options = null) { } public TableServiceClient(System.Uri endpoint, Azure.Data.Tables.TableSharedKeyCredential credential) { } diff --git a/sdk/tables/Azure.Data.Tables/samples/Sample1CreateDeleteTables.md b/sdk/tables/Azure.Data.Tables/samples/Sample1CreateDeleteTables.md index 6b0e228e92a8..611b6431d351 100644 --- a/sdk/tables/Azure.Data.Tables/samples/Sample1CreateDeleteTables.md +++ b/sdk/tables/Azure.Data.Tables/samples/Sample1CreateDeleteTables.md @@ -25,8 +25,8 @@ var tableClient = serviceClient.GetTableClient(tableName); ```C# Snippet:TablesSample1CreateTableClient tableClient = new TableClient( - tableName, new Uri(storageUri), + tableName, new TableSharedKeyCredential(accountName, storageAccountKey)); ``` diff --git a/sdk/tables/Azure.Data.Tables/samples/Sample2CreateDeleteEntities.md b/sdk/tables/Azure.Data.Tables/samples/Sample2CreateDeleteEntities.md index 12e1b5ad33fb..e3fc20a433ea 100644 --- a/sdk/tables/Azure.Data.Tables/samples/Sample2CreateDeleteEntities.md +++ b/sdk/tables/Azure.Data.Tables/samples/Sample2CreateDeleteEntities.md @@ -14,8 +14,8 @@ var tableClient = serviceClient.GetTableClient(tableName); ```C# Snippet:TablesSample1CreateTableClient tableClient = new TableClient( - tableName, new Uri(storageUri), + tableName, new TableSharedKeyCredential(accountName, storageAccountKey)); ``` diff --git a/sdk/tables/Azure.Data.Tables/samples/Sample4QueryEntities.md b/sdk/tables/Azure.Data.Tables/samples/Sample4QueryEntities.md index 6aa4a1175feb..d3c6ef466a74 100644 --- a/sdk/tables/Azure.Data.Tables/samples/Sample4QueryEntities.md +++ b/sdk/tables/Azure.Data.Tables/samples/Sample4QueryEntities.md @@ -14,8 +14,8 @@ var tableClient = serviceClient.GetTableClient(tableName); ```C# Snippet:TablesSample1CreateTableClient tableClient = new TableClient( - tableName, new Uri(storageUri), + tableName, new TableSharedKeyCredential(accountName, storageAccountKey)); ``` diff --git a/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj b/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj index 44dbd456d22a..04183d64ec75 100644 --- a/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj +++ b/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj @@ -2,7 +2,7 @@ This client library enables working with the Microsoft Azure Table service Microsoft Azure.Data.Tables client library - 1.0.0-preview.1 + 3.0.0-beta.1 TableSDK;$(DefineConstants) Microsoft Azure Tables;Microsoft;Azure;Tables;Table;$(PackageCommonTags) $(RequiredTargetFrameworks) @@ -26,6 +26,7 @@ + diff --git a/sdk/tables/Azure.Data.Tables/src/TableClient.cs b/sdk/tables/Azure.Data.Tables/src/TableClient.cs index 7aaf49e6fae6..5f7ddf399312 100644 --- a/sdk/tables/Azure.Data.Tables/src/TableClient.cs +++ b/sdk/tables/Azure.Data.Tables/src/TableClient.cs @@ -32,17 +32,17 @@ public class TableClient /// /// Initializes a new instance of the . /// - /// The name of the table with which this client instance will interact. /// /// A referencing the table service account. /// This is likely to be similar to "https://{account_name}.table.core.windows.net/" or "https://{account_name}.table.cosmos.azure.com/". /// + /// The name of the table with which this client instance will interact. /// /// Optional client options that define the transport pipeline policies for authentication, retries, etc., that are applied to every request. /// /// is not https. - public TableClient(string tableName, Uri endpoint, TableClientOptions options = null) - : this(tableName, endpoint, default(TableSharedKeyPipelinePolicy), options) + public TableClient(Uri endpoint, string tableName, TableClientOptions options = null) + : this(endpoint, tableName, default(TableSharedKeyPipelinePolicy), options) { Argument.AssertNotNull(tableName, nameof(tableName)); @@ -55,15 +55,15 @@ public TableClient(string tableName, Uri endpoint, TableClientOptions options = /// /// Initializes a new instance of the . /// - /// The name of the table with which this client instance will interact. /// /// A referencing the table service account. /// This is likely to be similar to "https://{account_name}.table.core.windows.net/" or "https://{account_name}.table.cosmos.azure.com/". /// + /// The name of the table with which this client instance will interact. /// The shared key credential used to sign requests. /// or is null. - public TableClient(string tableName, Uri endpoint, TableSharedKeyCredential credential) - : this(tableName, endpoint, new TableSharedKeyPipelinePolicy(credential), null) + public TableClient(Uri endpoint, string tableName, TableSharedKeyCredential credential) + : this(endpoint, tableName, new TableSharedKeyPipelinePolicy(credential), null) { Argument.AssertNotNull(tableName, nameof(tableName)); Argument.AssertNotNull(credential, nameof(credential)); @@ -72,24 +72,83 @@ public TableClient(string tableName, Uri endpoint, TableSharedKeyCredential cred /// /// Initializes a new instance of the . /// - /// The name of the table with which this client instance will interact. /// /// A referencing the table service account. /// This is likely to be similar to "https://{account_name}.table.core.windows.net/" or "https://{account_name}.table.cosmos.azure.com/". /// + /// The name of the table with which this client instance will interact. /// The shared key credential used to sign requests. /// /// Optional client options that define the transport pipeline policies for authentication, retries, etc., that are applied to every request. /// /// or is null. - public TableClient(string tableName, Uri endpoint, TableSharedKeyCredential credential, TableClientOptions options = null) - : this(tableName, endpoint, new TableSharedKeyPipelinePolicy(credential), options) + public TableClient(Uri endpoint, string tableName, TableSharedKeyCredential credential, TableClientOptions options = null) + : this(endpoint, tableName, new TableSharedKeyPipelinePolicy(credential), options) { Argument.AssertNotNull(tableName, nameof(tableName)); Argument.AssertNotNull(credential, nameof(credential)); } - internal TableClient(string tableName, Uri endpoint, TableSharedKeyPipelinePolicy policy, TableClientOptions options) + + /// + /// Initializes a new instance of the . + /// + /// + /// A connection string includes the authentication information + /// required for your application to access data in an Azure Storage + /// account at runtime. + /// + /// For more information, + /// + /// Configure Azure Storage connection strings. + /// + /// The name of the table with which this client instance will interact. + public TableClient(string connectionString, string tableName) + : this(connectionString, tableName, default) + { } + + /// + /// Initializes a new instance of the . + /// + /// + /// A connection string includes the authentication information + /// required for your application to access data in an Azure Storage + /// account at runtime. + /// + /// For more information, + /// + /// Configure Azure Storage connection strings. + /// + /// The name of the table with which this client instance will interact. + /// + /// Optional client options that define the transport pipeline policies for authentication, retries, etc., that are applied to every request. + /// + public TableClient(string connectionString, string tableName, TableClientOptions options = null) + { + Argument.AssertNotNull(connectionString, nameof(connectionString)); + + TableConnectionString connString = TableConnectionString.Parse(connectionString); + + options ??= new TableClientOptions(); + var endpointString = connString.TableStorageUri.PrimaryUri.ToString(); + + TableSharedKeyPipelinePolicy policy = connString.Credentials switch + { + TableSharedKeyCredential credential => new TableSharedKeyPipelinePolicy(credential), + _ => default + }; + + HttpPipeline pipeline = HttpPipelineBuilder.Build(options, policy); + + _diagnostics = new ClientDiagnostics(options); + _tableOperations = new TableRestClient(_diagnostics, pipeline, endpointString); + _version = options.VersionString; + _table = tableName; + _format = OdataMetadataFormat.ApplicationJsonOdataMinimalmetadata; + _isPremiumEndpoint = TableServiceClient.IsPremiumEndpoint(connString.TableStorageUri.PrimaryUri); + } + + internal TableClient(Uri endpoint, string tableName, TableSharedKeyPipelinePolicy policy, TableClientOptions options) { Argument.AssertNotNull(tableName, nameof(tableName)); Argument.AssertNotNull(endpoint, nameof(endpoint)); @@ -103,7 +162,6 @@ internal TableClient(string tableName, Uri endpoint, TableSharedKeyPipelinePolic _table = tableName; _format = OdataMetadataFormat.ApplicationJsonOdataMinimalmetadata; _isPremiumEndpoint = TableServiceClient.IsPremiumEndpoint(endpoint); - ; } internal TableClient(string table, TableRestClient tableOperations, string version, ClientDiagnostics diagnostics, bool isPremiumEndpoint) diff --git a/sdk/tables/Azure.Data.Tables/src/TableConnectionString.cs b/sdk/tables/Azure.Data.Tables/src/TableConnectionString.cs new file mode 100644 index 000000000000..6b80452fc4fc --- /dev/null +++ b/sdk/tables/Azure.Data.Tables/src/TableConnectionString.cs @@ -0,0 +1,446 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Azure.Core; +using AccountSetting = System.Collections.Generic.KeyValuePair>; +using ConnectionStringFilter = System.Func, System.Collections.Generic.IDictionary>; + +namespace Azure.Data.Tables +{ + internal class TableConnectionString + { + /// + /// Gets the endpoints for the Table service at the primary and secondary location, as configured for the storage account. + /// + /// A containing the Table service endpoints. + public (Uri PrimaryUri, Uri SecondaryUri) TableStorageUri { get; set; } + + /// + /// Gets the credentials used to create this object. + /// + /// A StorageCredentials object. + public object Credentials { get; set; } + + /// + /// True if the user used a constructor that auto-generates endpoints. + /// + internal bool DefaultEndpoints { get; set; } + + /// + /// The storage service hostname suffix set by the user, if any. + /// + internal string EndpointSuffix { get; set; } + + /// + /// The connection string parsed into settings. + /// + internal ConnectionString Settings { get; set; } + + /// + /// Indicates whether this account is a development storage account. + /// + internal bool IsDevStoreAccount { get; set; } + + /// + /// Private record of the account name for use in ToString(bool). + /// + internal string _accountName; + + /// + /// Singleton instance for the development storage account. + /// + private static TableConnectionString s_devStoreAccount; + + /// + /// Initializes a new instance of the class using the specified + /// account credentials and service endpoints. + /// + /// A StorageCredentials object. + /// A specifying the Table service endpoint or endpoints. + public TableConnectionString( + object storageCredentials, + (Uri, Uri) tableStorageUri) + { + Credentials = storageCredentials; + TableStorageUri = tableStorageUri; + DefaultEndpoints = false; + } + + /// + /// Gets a object that references the well-known development storage account. + /// + /// A object representing the development storage account. + public static TableConnectionString DevelopmentStorageAccount + { + get + { + if (s_devStoreAccount == null) + { + s_devStoreAccount = GetDevelopmentStorageAccount(null); + } + + return s_devStoreAccount; + } + } + + /// + /// Parses a connection string and returns a created + /// from the connection string. + /// + /// A valid connection string. + /// Thrown if is null or empty. + /// Thrown if is not a valid connection string. + /// Thrown if cannot be parsed. + /// A object constructed from the values provided in the connection string. + public static TableConnectionString Parse(string connectionString) + { + Argument.AssertNotNullOrEmpty(connectionString, nameof(connectionString)); + + if (ParseInternal(connectionString, out TableConnectionString ret, out string err)) + { + if (err != null) + { + throw new FormatException(err); + } + return ret; + } + + throw new ArgumentException("Connection string parsing error"); + } + + /// + /// Indicates whether a connection string can be parsed to return a object. + /// + /// The connection string to parse. + /// A object to hold the instance returned if + /// the connection string can be parsed. + /// true if the connection string was successfully parsed; otherwise, false. + public static bool TryParse(string connectionString, out TableConnectionString account) + { + if (string.IsNullOrEmpty(connectionString)) + { + account = null; + return false; + } + + try + { + return ParseInternal(connectionString, out account, out var error); + } + catch (Exception) + { + account = null; + return false; + } + } + + /// + /// Internal implementation of Parse/TryParse. + /// + /// The string to parse. + /// The to return. + /// A callback for reporting errors. + /// If true, the parse was successful. Otherwise, false. + internal static bool ParseInternal(string connectionString, out TableConnectionString accountInformation, out string error) + { + error = null; + + ConnectionString settings = ConnectionString.Parse(connectionString); + + // malformed settings string + + if (settings == null) + { + accountInformation = null; + error = "Unable to parse connection string."; + return false; + } + + // helper method + + string settingOrDefault(string key) + { + settings.TryGetSegmentValue(key, out var result); + return result; + } + + // devstore case + + if (settings.TryGetSegmentValue(TableConstants.ConnectionStrings.UseDevelopmentSetting, out var useDevSettings) && useDevSettings == "true") + { + accountInformation = + settings.TryGetSegmentValue(TableConstants.ConnectionStrings.DevelopmentProxyUriSetting, out var proxyUri) + ? GetDevelopmentStorageAccount(new Uri(proxyUri)) + : DevelopmentStorageAccount; + + return true; + } + + // non-devstore case + string sasToken = default; + + var matchesExplicitEndpointsSpec = + settings.TryGetSegmentValue(TableConstants.ConnectionStrings.TableEndpointSetting, out var tableEndpoint) && + Uri.IsWellFormedUriString(tableEndpoint, UriKind.Absolute) && + settings.TryGetSegmentValue(TableConstants.ConnectionStrings.SharedAccessSignatureSetting, out sasToken); + + var matchesAutomaticEndpointsSpec = settings.TryGetSegmentValue(TableConstants.ConnectionStrings.AccountNameSetting, out var accountName) && + settings.TryGetSegmentValue(TableConstants.ConnectionStrings.AccountKeySetting, out var accountKey) && + (settings.TryGetSegmentValue(TableConstants.ConnectionStrings.TableEndpointSetting, out accountName) || + settings.TryGetSegmentValue(TableConstants.ConnectionStrings.EndpointSuffixSetting, out var endpointSuffix)); + + if (matchesAutomaticEndpointsSpec || matchesExplicitEndpointsSpec) + { + if (matchesAutomaticEndpointsSpec && !settings.ContainsSegmentKey(TableConstants.ConnectionStrings.DefaultEndpointsProtocolSetting)) + { + settings.Add(TableConstants.ConnectionStrings.DefaultEndpointsProtocolSetting, "https"); + } + + var tableSecondaryEndpoint = settingOrDefault(TableConstants.ConnectionStrings.TableSecondaryEndpointSetting); + + // if secondary is specified, primary must also be specified + + static bool IsValidEndpointPair(string primary, string secondary) => + !string.IsNullOrWhiteSpace(primary) + || /* primary is null, and... */ string.IsNullOrWhiteSpace(secondary); + + (Uri, Uri) createStorageUri(string primary, string secondary, string sasToken, Func factory) + { + if (!string.IsNullOrWhiteSpace(primary)) + { + return (CreateUri(primary, sasToken), CreateUri(secondary, sasToken)); + } + else if (matchesAutomaticEndpointsSpec && factory != null) + { + return factory(settings); + } + else + { + return (default, default); + } + + static Uri CreateUri(string endpoint, string sasToken) + { + if (string.IsNullOrWhiteSpace(endpoint)) + { + return default; + } + var builder = new UriBuilder(endpoint); + if (!string.IsNullOrEmpty(builder.Query)) + { + builder.Query += "&" + sasToken; + } + else + { + builder.Query = sasToken; + } + return builder.Uri; + } + } + + if (IsValidEndpointPair(tableEndpoint, tableSecondaryEndpoint)) + { + accountInformation = + new TableConnectionString( + GetCredentials(settings), + tableStorageUri: createStorageUri(tableEndpoint, tableSecondaryEndpoint, sasToken ?? string.Empty, ConstructTableEndpoint) + ) + { + EndpointSuffix = settingOrDefault(TableConstants.ConnectionStrings.EndpointSuffixSetting), + Settings = settings + }; + + accountInformation._accountName = settingOrDefault(TableConstants.ConnectionStrings.AccountNameSetting); + + return true; + } + } + + // not valid + accountInformation = null; + error = "No valid combination of account information found."; + return false; + } + + /// + /// Returns a with development storage credentials using the specified proxy Uri. + /// + /// The proxy endpoint to use. + /// The new . + private static TableConnectionString GetDevelopmentStorageAccount(Uri proxyUri) + { + UriBuilder builder = proxyUri != null ? + new UriBuilder(proxyUri.Scheme, proxyUri.Host) : + new UriBuilder("http", TableConstants.ConnectionStrings.Localhost); + + builder.Path = TableConstants.ConnectionStrings.DevStoreAccountName; + + builder.Port = TableConstants.ConnectionStrings.TableEndpointPortNumber; + Uri tableEndpoint = builder.Uri; + + builder.Path = TableConstants.ConnectionStrings.DevStoreAccountName + TableConstants.ConnectionStrings.SecondaryLocationAccountSuffix; + + builder.Port = TableConstants.ConnectionStrings.TableEndpointPortNumber; + Uri tableSecondaryEndpoint = builder.Uri; + + var credentials = new TableSharedKeyCredential(TableConstants.ConnectionStrings.DevStoreAccountName, TableConstants.ConnectionStrings.DevStoreAccountKey); + var account = new TableConnectionString( + credentials, + tableStorageUri: (tableEndpoint, tableSecondaryEndpoint)) + { + Settings = ConnectionString.Empty() + }; + account.Settings.Add(TableConstants.ConnectionStrings.UseDevelopmentSetting, "true"); + if (proxyUri != null) + { + account.Settings.Add(TableConstants.ConnectionStrings.DevelopmentProxyUriSetting, proxyUri.ToString()); + } + + account.IsDevStoreAccount = true; + + return account; + } + + /// + /// Tokenizes input and stores name value pairs. + /// + /// The string to parse. + /// Error reporting delegate. + /// Tokenized collection. + private static IDictionary ParseStringIntoSettings(string connectionString, Action error) + { + IDictionary settings = new Dictionary(); + var splitted = connectionString.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + + foreach (var nameValue in splitted) + { + var splittedNameValue = nameValue.Split(new char[] { '=' }, 2); + + if (splittedNameValue.Length != 2) + { + error("Settings must be of the form \"name=value\"."); + return null; + } + + if (settings.ContainsKey(splittedNameValue[0])) + { + error(string.Format(CultureInfo.InvariantCulture, "Duplicate setting '{0}' found.", splittedNameValue[0])); + return null; + } + + settings.Add(splittedNameValue[0], splittedNameValue[1]); + } + + return settings; + } + + /// + /// Gets a StorageCredentials object corresponding to whatever credentials are supplied in the given settings. + /// + /// The settings to check. + /// The StorageCredentials object specified in the settings. + private static object GetCredentials(ConnectionString settings) + { + + settings.TryGetSegmentValue(TableConstants.ConnectionStrings.AccountNameSetting, out var accountName); + settings.TryGetSegmentValue(TableConstants.ConnectionStrings.AccountKeySetting, out var accountKey); + settings.TryGetSegmentValue(TableConstants.ConnectionStrings.SharedAccessSignatureSetting, out var sharedAccessSignature); + + return + accountName != null && accountKey != null && sharedAccessSignature == null + ? new TableSharedKeyCredential(accountName, accountKey) + : (object)(accountKey == null && sharedAccessSignature != null + ? sharedAccessSignature + : null); + } + + /// + /// Construct the Primary/Secondary Uri tuple. + /// + /// The protocol to use. + /// The name of the storage account. + /// Prefix that appears before the host name, e.g. "blob". + /// The Endpoint DNS suffix; use null for default. + /// The sas token; use null for default. + /// + private static (Uri, Uri) ConstructUris( + string scheme, + string accountName, + string hostNamePrefix, + string endpointSuffix, + string sasToken) + { + var primaryUriBuilder = new UriBuilder + { + Scheme = scheme, + Host = string.Format( + CultureInfo.InvariantCulture, + "{0}.{1}.{2}", + accountName, + hostNamePrefix, + endpointSuffix), + Query = sasToken + }; + + var secondaryUriBuilder = new UriBuilder + { + Scheme = scheme, + Host = string.Format( + CultureInfo.InvariantCulture, + "{0}{1}.{2}.{3}", + accountName, + TableConstants.ConnectionStrings.SecondaryLocationAccountSuffix, + hostNamePrefix, + endpointSuffix), + Query = sasToken + }; + + return (primaryUriBuilder.Uri, secondaryUriBuilder.Uri); + } + + /// + /// Gets the default queue endpoint using the specified protocol and account name. + /// + /// The protocol to use. + /// The name of the storage account. + /// The Endpoint DNS suffix; use null for default. + /// The sas token; use null for default. + /// The default table endpoint. + internal static (Uri, Uri) ConstructTableEndpoint(string scheme, string accountName, string endpointSuffix, string sasToken) + { + if (string.IsNullOrEmpty(scheme)) + { + throw Errors.ArgumentNull(nameof(scheme)); + } + + if (string.IsNullOrEmpty(accountName)) + { + throw Errors.ArgumentNull(nameof(accountName)); + } + + if (string.IsNullOrEmpty(endpointSuffix)) + { + endpointSuffix = TableConstants.ConnectionStrings.DefaultEndpointSuffix; + } + + return ConstructUris(scheme, accountName, TableConstants.ConnectionStrings.DefaultTableHostnamePrefix, endpointSuffix, sasToken); + } + + + /// + /// Gets the default table endpoint using the specified settings. + /// + /// The settings. + /// The default table endpoint. + private static (Uri, Uri) ConstructTableEndpoint(ConnectionString settings) => ConstructTableEndpoint( + settings.GetRequired(TableConstants.ConnectionStrings.DefaultEndpointsProtocolSetting), + settings.GetRequired(TableConstants.ConnectionStrings.AccountNameSetting), + settings.GetNonRequired(TableConstants.ConnectionStrings.EndpointSuffixSetting), + settings.GetNonRequired(TableConstants.ConnectionStrings.SharedAccessSignatureSetting)); + + } +} diff --git a/sdk/tables/Azure.Data.Tables/src/TableConstants.cs b/sdk/tables/Azure.Data.Tables/src/TableConstants.cs index 9d76f1206edd..d2529fa3bcf3 100644 --- a/sdk/tables/Azure.Data.Tables/src/TableConstants.cs +++ b/sdk/tables/Azure.Data.Tables/src/TableConstants.cs @@ -128,6 +128,31 @@ internal static class TableAccountServices { public const string Table = "t"; } + + } + /// + /// Table Connection String constant values. + /// + internal static class ConnectionStrings + { + internal const int TableEndpointPortNumber = 10002; + internal const string UseDevelopmentSetting = "UseDevelopmentStorage"; + internal const string DevelopmentProxyUriSetting = "DevelopmentStorageProxyUri"; + internal const string DefaultEndpointsProtocolSetting = "DefaultEndpointsProtocol"; + internal const string AccountNameSetting = "AccountName"; + internal const string AccountKeyNameSetting = "AccountKeyName"; + internal const string AccountKeySetting = "AccountKey"; + internal const string TableEndpointSetting = "TableEndpoint"; + internal const string TableSecondaryEndpointSetting = "TableSecondaryEndpoint"; + internal const string EndpointSuffixSetting = "EndpointSuffix"; + internal const string SharedAccessSignatureSetting = "SharedAccessSignature"; + internal const string DevStoreAccountName = "devstoreaccount1"; + internal const string DevStoreAccountKey = + "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="; + internal const string SecondaryLocationAccountSuffix = "-secondary"; + internal const string DefaultEndpointSuffix = "core.windows.net"; + internal const string DefaultTableHostnamePrefix = "table"; + internal const string Localhost = "127.0.0.1"; } } } diff --git a/sdk/tables/Azure.Data.Tables/src/TableServiceClient.cs b/sdk/tables/Azure.Data.Tables/src/TableServiceClient.cs index f284cd434020..49e5856ec482 100644 --- a/sdk/tables/Azure.Data.Tables/src/TableServiceClient.cs +++ b/sdk/tables/Azure.Data.Tables/src/TableServiceClient.cs @@ -34,6 +34,22 @@ public TableServiceClient(Uri endpoint) : this(endpoint, options: null) { } + /// + /// Initializes a new instance of the . + /// + /// + /// A connection string includes the authentication information + /// required for your application to access data in an Azure Storage + /// account at runtime. + /// + /// For more information, + /// + /// Configure Azure Storage connection strings. + /// + public TableServiceClient(string connectionString) + : this(connectionString, options: null) + { } + /// /// Initializes a new instance of the . /// @@ -84,6 +100,46 @@ public TableServiceClient(Uri endpoint, TableSharedKeyCredential credential, Tab Argument.AssertNotNull(credential, nameof(credential)); } + /// + /// Initializes a new instance of the . + /// + /// + /// A connection string includes the authentication information + /// required for your application to access data in an Azure Storage + /// account at runtime. + /// + /// For more information, + /// + /// Configure Azure Storage connection strings. + /// + /// + /// Optional client options that define the transport pipeline policies for authentication, retries, etc., that are applied to every request. + /// + public TableServiceClient(string connectionString, TableClientOptions options = null) + { + Argument.AssertNotNull(connectionString, nameof(connectionString)); + + TableConnectionString connString = TableConnectionString.Parse(connectionString); + + options ??= new TableClientOptions(); + var endpointString = connString.TableStorageUri.PrimaryUri.ToString(); + var secondaryEndpoint = connString.TableStorageUri.PrimaryUri?.ToString() ?? endpointString.Insert(endpointString.IndexOf('.'), "-secondary"); + + TableSharedKeyPipelinePolicy policy = connString.Credentials switch + { + TableSharedKeyCredential credential => new TableSharedKeyPipelinePolicy(credential), + _ => default + }; + HttpPipeline pipeline = HttpPipelineBuilder.Build(options, policy); + + _diagnostics = new ClientDiagnostics(options); + _tableOperations = new TableRestClient(_diagnostics, pipeline, endpointString); + _serviceOperations = new ServiceRestClient(_diagnostics, pipeline, endpointString); + _secondaryServiceOperations = new ServiceRestClient(_diagnostics, pipeline, secondaryEndpoint); + _version = options.VersionString; + _isPremiumEndpoint = IsPremiumEndpoint(connString.TableStorageUri.PrimaryUri); + } + internal TableServiceClient(Uri endpoint, TableSharedKeyPipelinePolicy policy, TableClientOptions options) { Argument.AssertNotNull(endpoint, nameof(endpoint)); diff --git a/sdk/tables/Azure.Data.Tables/tests/TableClientTests.cs b/sdk/tables/Azure.Data.Tables/tests/TableClientTests.cs index 5a754eecfd70..4aaeb195e308 100644 --- a/sdk/tables/Azure.Data.Tables/tests/TableClientTests.cs +++ b/sdk/tables/Azure.Data.Tables/tests/TableClientTests.cs @@ -41,21 +41,21 @@ public void TestSetup() [Test] public void ConstructorValidatesArguments() { - Assert.That(() => new TableClient(null, _url, new TableSharedKeyCredential(AccountName, string.Empty)), Throws.InstanceOf(), "The constructor should validate the tableName."); + Assert.That(() => new TableClient(_url, null, new TableSharedKeyCredential(AccountName, string.Empty)), Throws.InstanceOf(), "The constructor should validate the tableName."); - Assert.That(() => new TableClient(TableName, null, new TableSharedKeyCredential(AccountName, string.Empty)), Throws.InstanceOf(), "The constructor should validate the url."); + Assert.That(() => new TableClient(null, TableName, new TableSharedKeyCredential(AccountName, string.Empty)), Throws.InstanceOf(), "The constructor should validate the url."); - Assert.That(() => new TableClient(TableName, _url, new TableSharedKeyCredential(AccountName, string.Empty), new TableClientOptions()), Throws.Nothing, "The constructor should accept valid arguments."); + Assert.That(() => new TableClient(_url, TableName, new TableSharedKeyCredential(AccountName, string.Empty), new TableClientOptions()), Throws.Nothing, "The constructor should accept valid arguments."); - Assert.That(() => new TableClient(TableName, _url, credential: null), Throws.InstanceOf(), "The constructor should validate the TablesSharedKeyCredential."); + Assert.That(() => new TableClient(_url, TableName, credential: null), Throws.InstanceOf(), "The constructor should validate the TablesSharedKeyCredential."); - Assert.That(() => new TableClient(TableName, _urlHttp), Throws.InstanceOf(), "The constructor should validate the Uri is https when using a SAS token."); + Assert.That(() => new TableClient(_urlHttp, TableName), Throws.InstanceOf(), "The constructor should validate the Uri is https when using a SAS token."); - Assert.That(() => new TableClient(TableName, _url), Throws.Nothing, "The constructor should accept a null credential"); + Assert.That(() => new TableClient(_url, TableName), Throws.Nothing, "The constructor should accept a null credential"); - Assert.That(() => new TableClient(TableName, _url, new TableSharedKeyCredential(AccountName, string.Empty)), Throws.Nothing, "The constructor should accept valid arguments."); + Assert.That(() => new TableClient(_url, TableName, new TableSharedKeyCredential(AccountName, string.Empty)), Throws.Nothing, "The constructor should accept valid arguments."); - Assert.That(() => new TableClient(TableName, _urlHttp, new TableSharedKeyCredential(AccountName, string.Empty)), Throws.Nothing, "The constructor should accept an http url."); + Assert.That(() => new TableClient(_urlHttp, TableName, new TableSharedKeyCredential(AccountName, string.Empty)), Throws.Nothing, "The constructor should accept an http url."); } /// diff --git a/sdk/tables/Azure.Data.Tables/tests/TableConnectionStringTests.cs b/sdk/tables/Azure.Data.Tables/tests/TableConnectionStringTests.cs new file mode 100644 index 000000000000..292e6c223dde --- /dev/null +++ b/sdk/tables/Azure.Data.Tables/tests/TableConnectionStringTests.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Azure.Data.Tables; +using NUnit.Framework; + +namespace Azure.Tables.Tests +{ + public class TableConnectionStringTests + { + private const string AccountName = "accountName"; + private const string SasToken = "sv=2019-12-12&ss=t&srt=s&sp=rwdlacu&se=2020-08-28T23:45:30Z&st=2020-08-26T15:45:30Z&spr=https&sig=mySig"; + private const string Secret = "Kg=="; + private readonly TableSharedKeyCredential _expectedCred = new TableSharedKeyCredential(AccountName, Secret); + private readonly TableSharedKeyCredential _expectedDevStoraageCred = new TableSharedKeyCredential(TableConstants.ConnectionStrings.DevStoreAccountName, TableConstants.ConnectionStrings.DevStoreAccountKey); + + /// + /// Validates the functionality of the TableConnectionString. + /// + [Test] + public void ParsesDevStorage() + { + var connString = $"UseDevelopmentStorage=true"; + + Assert.That(TableConnectionString.TryParse(connString, out TableConnectionString tcs), "Parsing should have been successful"); + Assert.That(tcs.Credentials, Is.Not.Null); + Assert.That(GetCredString(tcs.Credentials), Is.EqualTo(GetExpectedHash(_expectedDevStoraageCred)), "The Credentials should have matched."); + Assert.That(tcs.TableStorageUri.PrimaryUri, Is.EqualTo(new Uri($"http://{TableConstants.ConnectionStrings.Localhost}:{TableConstants.ConnectionStrings.TableEndpointPortNumber}/{TableConstants.ConnectionStrings.DevStoreAccountName}")), "The PrimaryUri should have matched."); + } + + public static IEnumerable ValidStorageConnStrings() + { + yield return new object[] { $"DefaultEndpointsProtocol=https;AccountName={AccountName};AccountKey={Secret};EndpointSuffix=core.windows.net" }; + yield return new object[] { $"AccountName={AccountName};AccountKey={Secret};EndpointSuffix=core.windows.net" }; + } + + /// + /// Validates the functionality of the TableConnectionString. + /// + [Test] + [TestCaseSource(nameof(ValidStorageConnStrings))] + public void ParsesStorage(string connString) + { + Assert.That(TableConnectionString.TryParse(connString, out TableConnectionString tcs), "Parsing should have been successful"); + Assert.That(tcs.Credentials, Is.Not.Null); + Assert.That(GetCredString(tcs.Credentials), Is.EqualTo(GetExpectedHash(_expectedCred)), "The Credentials should have matched."); + Assert.That(tcs.TableStorageUri.PrimaryUri, Is.EqualTo(new Uri($"https://{AccountName}.table.core.windows.net/")), "The PrimaryUri should have matched."); + } + + public static IEnumerable ValidCosmosConnStrings() + { + yield return new object[] { $"DefaultEndpointsProtocol=https;AccountName={AccountName};AccountKey={Secret};TableEndpoint=https://{AccountName}.table.cosmos.azure.com:443/;" }; + yield return new object[] { $"AccountName={AccountName};AccountKey={Secret};TableEndpoint=https://{AccountName}.table.cosmos.azure.com:443/;" }; + } + + /// + /// Validates the functionality of the TableConnectionString. + /// + [Test] + [TestCaseSource(nameof(ValidCosmosConnStrings))] + public void ParsesCosmos(string connString) + { + Assert.That(TableConnectionString.TryParse(connString, out TableConnectionString tcs), "Parsing should have been successful"); + Assert.That(tcs.Credentials, Is.Not.Null); + Assert.That(GetCredString(tcs.Credentials), Is.EqualTo(GetExpectedHash(_expectedCred)), "The Credentials should have matched."); + Assert.That(tcs.TableStorageUri.PrimaryUri, Is.EqualTo(new Uri($"https://{AccountName}.table.cosmos.azure.com:443/")), "The PrimaryUri should have matched."); + } + + /// + /// Validates the functionality of the TableConnectionString. + /// + [Test] + public void ParsesSaS() + { + var connString = $"BlobEndpoint=https://{AccountName}.blob.core.windows.net/;QueueEndpoint=https://{AccountName}.queue.core.windows.net/;FileEndpoint=https://{AccountName}.file.core.windows.net/;TableEndpoint=https://{AccountName}.table.core.windows.net/;SharedAccessSignature={SasToken}"; + + Assert.That(TableConnectionString.TryParse(connString, out TableConnectionString tcs), "Parsing should have been successful"); + Assert.That(tcs.Credentials, Is.Not.Null); + Assert.That(GetCredString(tcs.Credentials), Is.EqualTo(SasToken), "The Credentials should have matched."); + Assert.That(tcs.TableStorageUri.PrimaryUri, Is.EqualTo(new Uri($"https://{AccountName}.table.core.windows.net/?{SasToken}")), "The PrimaryUri should have matched."); + } + + public static IEnumerable InvalidConnStrings() + { + yield return new object[] { "UseDevelopmentStorage=false" }; + yield return new object[] { $"BlobEndpoint=https://{AccountName}.blob.core.windows.net/;QueueEndpoint=https://{AccountName}.queue.core.windows.net/;FileEndpoint=https://{AccountName}.file.core.windows.net/;TableEndpoint=https://{AccountName}.table.core.windows.net/" }; + yield return new object[] { $"DefaultEndpointsProtocol=https;AccountName={AccountName};AccountKey={Secret}" }; + yield return new object[] { $"DefaultEndpointsProtocol=https;AccountName={AccountName};EndpointSuffix=core.windows.net" }; + yield return new object[] { $"DefaultEndpointsProtocol=https;AccountKey={Secret};EndpointSuffix=core.windows.net" }; + } + + /// + /// Validates the functionality of the TableConnectionString. + /// + [Test] + [TestCaseSource(nameof(InvalidConnStrings))] + public void ParseFailsWithInvalidConnString(string connString) + { + Assert.That(TableConnectionString.TryParse(connString, out TableConnectionString tcs), Is.False, "Parsing should not have been successful"); + } + + private string GetExpectedHash(TableSharedKeyCredential cred) => cred.ComputeHMACSHA256("message"); + + private string GetCredString(object credential) => credential switch + { + TableSharedKeyCredential cred => GetExpectedHash(cred), + string sas => sas, + _ => null + }; + } +} diff --git a/sdk/tables/Azure.Data.Tables/tests/TableServiceLiveTestsBase.cs b/sdk/tables/Azure.Data.Tables/tests/TableServiceLiveTestsBase.cs index f2a26423dd3b..942561e9b73e 100644 --- a/sdk/tables/Azure.Data.Tables/tests/TableServiceLiveTestsBase.cs +++ b/sdk/tables/Azure.Data.Tables/tests/TableServiceLiveTestsBase.cs @@ -51,6 +51,7 @@ public TableServiceLiveTestsBase(bool isAsync, TableEndpointType endpointType) : protected string ServiceUri; protected string AccountName; protected string AccountKey; + protected string ConnectionString; /// /// Creates a with the endpoint and API key provided via environment @@ -59,22 +60,6 @@ public TableServiceLiveTestsBase(bool isAsync, TableEndpointType endpointType) : [SetUp] public async Task TablesTestSetup() { - service = _endpointType switch - { - - TableEndpointType.Storage => InstrumentClient(new TableServiceClient( - new Uri(TestEnvironment.StorageUri), - new TableSharedKeyCredential(TestEnvironment.StorageAccountName, TestEnvironment.PrimaryStorageAccountKey), - Recording.InstrumentClientOptions(new TableClientOptions()))), - - TableEndpointType.CosmosTable => InstrumentClient(new TableServiceClient( - new Uri(TestEnvironment.CosmosUri), - new TableSharedKeyCredential(TestEnvironment.CosmosAccountName, TestEnvironment.PrimaryCosmosAccountKey), - Recording.InstrumentClientOptions(new TableClientOptions()))), - - _ => throw new NotSupportedException("Unknown endpoint type") - - }; ServiceUri = _endpointType switch { @@ -97,6 +82,11 @@ public async Task TablesTestSetup() _ => throw new NotSupportedException("Unknown endpoint type") }; + service = InstrumentClient(new TableServiceClient( + new Uri(ServiceUri), + new TableSharedKeyCredential(AccountName, AccountKey), + Recording.InstrumentClientOptions(new TableClientOptions()))); + tableName = Recording.GenerateAlphaNumericId("testtable", useOnlyLowercase: true); await CosmosThrottleWrapper(async () => await service.CreateTableAsync(tableName).ConfigureAwait(false)); diff --git a/sdk/tables/Azure.Data.Tables/tests/TablesTestEnvironment.cs b/sdk/tables/Azure.Data.Tables/tests/TablesTestEnvironment.cs index 1ed587c753a4..d73e62f61380 100644 --- a/sdk/tables/Azure.Data.Tables/tests/TablesTestEnvironment.cs +++ b/sdk/tables/Azure.Data.Tables/tests/TablesTestEnvironment.cs @@ -12,17 +12,15 @@ public TablesTestEnvironment() : base("tables") } // Storage Tables - public const string PrimaryStorageKeyEnvironmentVariableName = "TABLES_PRIMARY_STORAGE_ACCOUNT_KEY"; - private const string StorageUriFormat = "https://{0}.table.core.windows.net"; - public string PrimaryStorageAccountKey => GetRecordedVariable(PrimaryStorageKeyEnvironmentVariableName, options => options.IsSecret(SanitizedValue.Base64)); + public const string DefaultStorageSuffix = ".table.core.windows.net"; + public string PrimaryStorageAccountKey => GetRecordedVariable("TABLES_PRIMARY_STORAGE_ACCOUNT_KEY", options => options.IsSecret(SanitizedValue.Base64)); public string StorageAccountName => GetRecordedVariable("TABLES_STORAGE_ACCOUNT_NAME"); - public string StorageUri => string.Format(StorageUriFormat, StorageAccountName); + public string StorageUri => $"https://{StorageAccountName}{StorageEndpointSuffix ?? DefaultStorageSuffix}"; // Cosmos Tables - public const string PrimaryCosmosKeyEnvironmentVariableName = "TABLES_PRIMARY_COSMOS_ACCOUNT_KEY"; - private const string CosmosUriFormat = "https://{0}.table.cosmos.azure.com"; - public string PrimaryCosmosAccountKey => GetRecordedVariable(PrimaryCosmosKeyEnvironmentVariableName, options => options.IsSecret(SanitizedValue.Base64)); + public string CosmosEndpointSuffix => GetRecordedOptionalVariable("COSMOS_TABLES_ENDPOINT_SUFFIX") ?? ".table.cosmos.azure.com"; + public string PrimaryCosmosAccountKey => GetRecordedVariable("TABLES_PRIMARY_COSMOS_ACCOUNT_KEY", options => options.IsSecret(SanitizedValue.Base64)); public string CosmosAccountName => GetRecordedVariable("TABLES_COSMOS_ACCOUNT_NAME"); - public string CosmosUri => string.Format(CosmosUriFormat, CosmosAccountName); + public string CosmosUri => $"https://{CosmosAccountName}{CosmosEndpointSuffix}"; } } diff --git a/sdk/tables/Azure.Data.Tables/tests/samples/Sample1_CreateDeleteTable.cs b/sdk/tables/Azure.Data.Tables/tests/samples/Sample1_CreateDeleteTable.cs index 74c3b83b5d5f..bc23a68deb60 100644 --- a/sdk/tables/Azure.Data.Tables/tests/samples/Sample1_CreateDeleteTable.cs +++ b/sdk/tables/Azure.Data.Tables/tests/samples/Sample1_CreateDeleteTable.cs @@ -45,8 +45,8 @@ public void CreateDeleteTable() #region Snippet:TablesSample1CreateTableClient tableClient = new TableClient( - tableName, new Uri(storageUri), + tableName, new TableSharedKeyCredential(accountName, storageAccountKey)); #endregion diff --git a/sdk/tables/Azure.Data.Tables/tests/samples/Sample2_CreateDeleteEntities.cs b/sdk/tables/Azure.Data.Tables/tests/samples/Sample2_CreateDeleteEntities.cs index c83f23e5b71c..ddb15de74679 100644 --- a/sdk/tables/Azure.Data.Tables/tests/samples/Sample2_CreateDeleteEntities.cs +++ b/sdk/tables/Azure.Data.Tables/tests/samples/Sample2_CreateDeleteEntities.cs @@ -26,8 +26,8 @@ public void CreateDeleteEntity() #region Snippet:TablesSample2CreateTableWithTableClient // Construct a new using a . var tableClient = new TableClient( - tableName, new Uri(storageUri), + tableName, new TableSharedKeyCredential(accountName, storageAccountKey)); // Create the table in the service. diff --git a/sdk/tables/Azure.Data.Tables/tests/samples/Sample2_CreateDeleteEntitiesAsync.cs b/sdk/tables/Azure.Data.Tables/tests/samples/Sample2_CreateDeleteEntitiesAsync.cs index 3f13d12c90d2..2b0923a5bd2d 100644 --- a/sdk/tables/Azure.Data.Tables/tests/samples/Sample2_CreateDeleteEntitiesAsync.cs +++ b/sdk/tables/Azure.Data.Tables/tests/samples/Sample2_CreateDeleteEntitiesAsync.cs @@ -27,8 +27,8 @@ public async Task CreateDeleteEntitiesAsync() #region Snippet:TablesSample2CreateTableClientAsync // Construct a new using a . var client = new TableClient( - tableName, new Uri(storageUri), + tableName, new TableSharedKeyCredential(accountName, storageAccountKey)); // Create the table in the service.