diff --git a/extensions/Worker.Extensions.Shared/Configuration/ConfigurationExtensions.cs b/extensions/Worker.Extensions.Shared/Configuration/ConfigurationExtensions.cs
index cff348de4..ee4203f0b 100644
--- a/extensions/Worker.Extensions.Shared/Configuration/ConfigurationExtensions.cs
+++ b/extensions/Worker.Extensions.Shared/Configuration/ConfigurationExtensions.cs
@@ -1,6 +1,8 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
+using System;
+using System.Globalization;
using Microsoft.Extensions.Configuration;
namespace Microsoft.Azure.Functions.Worker.Extensions
@@ -30,6 +32,45 @@ internal static IConfigurationSection GetWebJobsConnectionStringSection(this ICo
return section;
}
+ ///
+ /// Either constructs the serviceUri from the provided accountName
+ /// or retrieves the serviceUri for the specific resource (i.e. blobServiceUri or queueServiceUri)
+ ///
+ /// configuration section for a given connection name
+ /// The subdomain of the serviceUri (i.e. blob, queue, table)
+ /// The serviceUri for the specific resource (i.e. blobServiceUri or queueServiceUri)
+ internal static bool TryGetServiceUriForStorageAccounts(this IConfiguration configuration, string subDomain, out Uri serviceUri)
+ {
+ if (subDomain is null)
+ {
+ throw new ArgumentNullException(nameof(subDomain));
+ }
+ var serviceUriConfig = string.Format(CultureInfo.InvariantCulture, "{0}ServiceUri", subDomain);
+
+ if (configuration.GetValue("accountName") is { } accountName)
+ {
+ serviceUri = FormatServiceUri(accountName, subDomain);
+ return true;
+ }
+ else if (configuration.GetValue($"{subDomain}ServiceUri") is { } uriStr)
+ {
+ serviceUri = new Uri(uriStr);
+ return true;
+ }
+
+ serviceUri = default(Uri)!;
+ return false;
+ }
+
+ ///
+ /// Generates the serviceUri for a particular storage resource
+ ///
+ private static Uri FormatServiceUri(string accountName, string subDomain, string defaultProtocol = "https", string endpointSuffix = "core.windows.net")
+ {
+ var uri = string.Format(CultureInfo.InvariantCulture, "{0}://{1}.{2}.{3}", defaultProtocol, accountName, subDomain, endpointSuffix);
+ return new Uri(uri);
+ }
+
///
/// Creates a WebJobs specific prefixed string using a given connection name.
///
@@ -58,4 +99,4 @@ private static IConfigurationSection GetConnectionStringOrSetting(this IConfigur
return configuration.GetSection(connectionName);
}
}
-}
\ No newline at end of file
+}
diff --git a/extensions/Worker.Extensions.Storage.Blobs/src/Config/BlobStorageBindingOptionsSetup.cs b/extensions/Worker.Extensions.Storage.Blobs/src/Config/BlobStorageBindingOptionsSetup.cs
index 654653702..0da09c4e3 100644
--- a/extensions/Worker.Extensions.Storage.Blobs/src/Config/BlobStorageBindingOptionsSetup.cs
+++ b/extensions/Worker.Extensions.Storage.Blobs/src/Config/BlobStorageBindingOptionsSetup.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
using System;
@@ -52,7 +52,7 @@ public void Configure(string name, BlobStorageBindingOptions options)
}
else
{
- if (TryGetServiceUri(connectionSection, out Uri serviceUri))
+ if (connectionSection.TryGetServiceUriForStorageAccounts(BlobServiceUriSubDomain, out Uri serviceUri))
{
options.ServiceUri = serviceUri;
}
@@ -61,39 +61,5 @@ public void Configure(string name, BlobStorageBindingOptions options)
options.BlobClientOptions = (BlobClientOptions)_componentFactory.CreateClientOptions(typeof(BlobClientOptions), null, connectionSection);
options.Credential = _componentFactory.CreateTokenCredential(connectionSection);
}
-
- ///
- /// Either constructs the serviceUri from the provided accountName
- /// or retrieves the serviceUri for the specific resource (i.e. blobServiceUri or queueServiceUri)
- ///
- private bool TryGetServiceUri(IConfiguration configuration, out Uri serviceUri)
- {
- var serviceUriConfig = string.Format(CultureInfo.InvariantCulture, "{0}ServiceUri", BlobServiceUriSubDomain);
-
- string accountName;
- string uriStr;
- if ((accountName = configuration.GetValue("accountName")) is not null)
- {
- serviceUri = FormatServiceUri(accountName);
- return true;
- }
- else if ((uriStr = configuration.GetValue(serviceUriConfig)) is not null)
- {
- serviceUri = new Uri(uriStr);
- return true;
- }
-
- serviceUri = default(Uri)!;
- return false;
- }
-
- ///
- /// Generates the serviceUri for a particular storage resource
- ///
- private Uri FormatServiceUri(string accountName, string defaultProtocol = "https", string endpointSuffix = "core.windows.net")
- {
- var uri = string.Format(CultureInfo.InvariantCulture, "{0}://{1}.{2}.{3}", defaultProtocol, accountName, BlobServiceUriSubDomain, endpointSuffix);
- return new Uri(uri);
- }
}
-}
\ No newline at end of file
+}
diff --git a/extensions/Worker.Extensions.Tables/release_notes.md b/extensions/Worker.Extensions.Tables/release_notes.md
index 62b6451d2..63cbbd189 100644
--- a/extensions/Worker.Extensions.Tables/release_notes.md
+++ b/extensions/Worker.Extensions.Tables/release_notes.md
@@ -6,4 +6,4 @@
### Microsoft.Azure.Functions.Worker.Extensions.Tables
--
+- Add abiility to bind table input to TableClient, TableEntity, and IEnumerable (#1470)
\ No newline at end of file
diff --git a/extensions/Worker.Extensions.Tables/src/Config/TablesBindingOptions.cs b/extensions/Worker.Extensions.Tables/src/Config/TablesBindingOptions.cs
new file mode 100644
index 000000000..bc91b2b8e
--- /dev/null
+++ b/extensions/Worker.Extensions.Tables/src/Config/TablesBindingOptions.cs
@@ -0,0 +1,41 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+using Azure.Core;
+using Azure.Data.Tables;
+
+namespace Microsoft.Azure.Functions.Worker.Extensions.Tables.Config
+{
+ internal class TablesBindingOptions
+ {
+ public string? ConnectionString { get; set; }
+
+ public Uri? ServiceUri { get; set; }
+
+ public TokenCredential? Credential { get; set; }
+
+ public TableClientOptions? TableClientOptions { get; set; }
+
+ private TableServiceClient? TableServiceClient;
+
+ internal virtual TableServiceClient CreateClient()
+ {
+ if (ConnectionString is null && ServiceUri is null)
+ {
+ throw new ArgumentNullException(nameof(ConnectionString) + " " + nameof(ServiceUri));
+ }
+
+ if (TableServiceClient is not null)
+ {
+ return TableServiceClient;
+ }
+
+ TableServiceClient = !string.IsNullOrEmpty(ConnectionString)
+ ? new TableServiceClient(ConnectionString, TableClientOptions) // Connection string based auth;
+ : new TableServiceClient(ServiceUri, Credential, TableClientOptions); // AAD auth
+
+ return TableServiceClient;
+ }
+ }
+}
diff --git a/extensions/Worker.Extensions.Tables/src/Config/TablesBindingOptionsSetup.cs b/extensions/Worker.Extensions.Tables/src/Config/TablesBindingOptionsSetup.cs
new file mode 100644
index 000000000..88d69b761
--- /dev/null
+++ b/extensions/Worker.Extensions.Tables/src/Config/TablesBindingOptionsSetup.cs
@@ -0,0 +1,59 @@
+using System;
+using Azure.Data.Tables;
+using Microsoft.Extensions.Azure;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Azure.Functions.Worker.Extensions.Tables.Config
+{
+ internal class TablesBindingOptionsSetup : IConfigureNamedOptions
+ {
+ private readonly IConfiguration _configuration;
+ private readonly AzureComponentFactory _componentFactory;
+
+ private const string TablesServiceUriSubDomain = "table";
+
+ public TablesBindingOptionsSetup(IConfiguration configuration, AzureComponentFactory componentFactory)
+ {
+ _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
+ _componentFactory = componentFactory ?? throw new ArgumentNullException(nameof(componentFactory));
+ }
+
+ public void Configure(TablesBindingOptions options)
+ {
+ Configure(Options.DefaultName, options);
+ }
+
+ public void Configure(string name, TablesBindingOptions options)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ name = Constants.Storage; // default
+ }
+
+ IConfigurationSection connectionSection = _configuration.GetWebJobsConnectionStringSection(name);
+
+ if (!connectionSection.Exists())
+ {
+ // Not found
+ throw new InvalidOperationException($"Tables connection configuration '{name}' does not exist. " +
+ "Make sure that it is a defined App Setting.");
+ }
+
+ if (!string.IsNullOrWhiteSpace(connectionSection.Value))
+ {
+ options.ConnectionString = connectionSection.Value;
+ }
+ else
+ {
+ if (connectionSection.TryGetServiceUriForStorageAccounts(TablesServiceUriSubDomain, out Uri serviceUri))
+ {
+ options.ServiceUri = serviceUri;
+ }
+ }
+
+ options.TableClientOptions = (TableClientOptions)_componentFactory.CreateClientOptions(typeof(TableClientOptions), null, connectionSection);
+ options.Credential = _componentFactory.CreateTokenCredential(connectionSection);
+ }
+ }
+}
diff --git a/extensions/Worker.Extensions.Tables/src/Constants.cs b/extensions/Worker.Extensions.Tables/src/Constants.cs
new file mode 100644
index 000000000..2f6667178
--- /dev/null
+++ b/extensions/Worker.Extensions.Tables/src/Constants.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Microsoft.Azure.Functions.Worker.Extensions.Tables
+{
+ internal class Constants
+ {
+ internal const string Storage = "Storage";
+ internal const string TablesExtensionName = "AzureStorageTables";
+ internal const string TableName = "TableName";
+ internal const string PartitionKey = "PartitionKey";
+ internal const string RowKey = "RowKey";
+ internal const string Connection = "Connection";
+ internal const string Take = "Take";
+ internal const string Filter = "Filter";
+ // Media content types
+ internal const string JsonContentType = "application/json";
+ }
+}
diff --git a/extensions/Worker.Extensions.Tables/src/Nuget.config b/extensions/Worker.Extensions.Tables/src/Nuget.config
new file mode 100644
index 000000000..5e150b6ee
--- /dev/null
+++ b/extensions/Worker.Extensions.Tables/src/Nuget.config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/Worker.Extensions.Tables/src/Properties/AssemblyInfo.cs b/extensions/Worker.Extensions.Tables/src/Properties/AssemblyInfo.cs
index 5ac8b0c07..540c7067e 100644
--- a/extensions/Worker.Extensions.Tables/src/Properties/AssemblyInfo.cs
+++ b/extensions/Worker.Extensions.Tables/src/Properties/AssemblyInfo.cs
@@ -1,6 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
-using Microsoft.Azure.Functions.Worker.Extensions.Abstractions;
+using System.Runtime.CompilerServices;
+using Microsoft.Azure.Functions.Worker.Extensions.Abstractions;
-[assembly: ExtensionInformation("Microsoft.Azure.WebJobs.Extensions.Tables", "1.0.0")]
+[assembly: ExtensionInformation("Microsoft.Azure.WebJobs.Extensions.Tables", "1.2.0-beta.1")]
+[assembly: InternalsVisibleTo("Microsoft.Azure.Functions.WorkerExtension.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001005148be37ac1d9f58bd40a2e472c9d380d635b6048278f7d47480b08c928858f0f7fe17a6e4ce98da0e7a7f0b8c308aecd9e9b02d7e9680a5b5b75ac7773cec096fbbc64aebd429e77cb5f89a569a79b28e9c76426783f624b6b70327eb37341eb498a2c3918af97c4860db6cdca4732787150841e395a29cfacb959c1fd971c1")]
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
diff --git a/extensions/Worker.Extensions.Tables/src/TableExtensionStartup.cs b/extensions/Worker.Extensions.Tables/src/TableExtensionStartup.cs
new file mode 100644
index 000000000..d20fd8d32
--- /dev/null
+++ b/extensions/Worker.Extensions.Tables/src/TableExtensionStartup.cs
@@ -0,0 +1,36 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Core;
+using Microsoft.Azure.Functions.Worker.Extensions.Tables.Config;
+using Microsoft.Extensions.Azure;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+
+[assembly: WorkerExtensionStartup(typeof(TableExtensionStartup))]
+
+namespace Microsoft.Azure.Functions.Worker
+{
+ ///
+ /// Table extension startup.
+ ///
+ public class TableExtensionStartup : WorkerExtensionStartup
+ {
+ ///
+ /// Configure table extension startup.
+ ///
+ public override void Configure(IFunctionsWorkerApplicationBuilder applicationBuilder)
+ {
+ if (applicationBuilder == null)
+ {
+ throw new ArgumentNullException(nameof(applicationBuilder));
+ }
+
+ applicationBuilder.Services.AddAzureClientsCore(); // Adds AzureComponentFactory
+ applicationBuilder.Services.AddOptions();
+ applicationBuilder.Services.AddSingleton, TablesBindingOptionsSetup>();
+ }
+ }
+}
diff --git a/extensions/Worker.Extensions.Tables/src/TableInputAttribute.cs b/extensions/Worker.Extensions.Tables/src/TableInputAttribute.cs
index b1d2f6d1b..125f8a5e6 100644
--- a/extensions/Worker.Extensions.Tables/src/TableInputAttribute.cs
+++ b/extensions/Worker.Extensions.Tables/src/TableInputAttribute.cs
@@ -1,13 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
+using Microsoft.Azure.Functions.Worker.Converters;
using Microsoft.Azure.Functions.Worker.Extensions.Abstractions;
+using Microsoft.Azure.Functions.Worker.Extensions.Tables.TypeConverters;
namespace Microsoft.Azure.Functions.Worker
{
///
/// Attribute used to configure a parameter as the input target for the Azure Storage Tables binding.
///
+ [AllowConverterFallback(true)]
+ [InputConverter(typeof(TableClientConverter))]
+ [InputConverter(typeof(TableEntityConverter))]
+ [InputConverter(typeof(TableEntityEnumerableConverter))]
public class TableInputAttribute : InputBindingAttribute
{
/// Initializes a new instance of the class.
@@ -37,6 +43,17 @@ public TableInputAttribute(string tableName, string partitionKey, string rowKey)
RowKey = rowKey;
}
+ /// Initializes a new instance of the class.
+ /// The name of the table containing the entity.
+ /// The partition key of the entity.
+ /// The number of entities to return
+ public TableInputAttribute(string tableName, string partitionKey, int take)
+ {
+ TableName = tableName;
+ PartitionKey = partitionKey;
+ Take = take;
+ }
+
/// Gets the name of the table to which to bind.
/// When binding to a table entity, gets the name of the table containing the entity.
public string TableName { get; }
diff --git a/extensions/Worker.Extensions.Tables/src/TypeConverters/TableClientConverter.cs b/extensions/Worker.Extensions.Tables/src/TypeConverters/TableClientConverter.cs
new file mode 100644
index 000000000..dc0a761c9
--- /dev/null
+++ b/extensions/Worker.Extensions.Tables/src/TypeConverters/TableClientConverter.cs
@@ -0,0 +1,65 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Azure.Data.Tables;
+using Microsoft.Azure.Functions.Worker.Converters;
+using Microsoft.Azure.Functions.Worker.Extensions.Abstractions;
+using Microsoft.Azure.Functions.Worker.Extensions.Tables.Config;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Logging;
+using Microsoft.Azure.Functions.Worker.Core;
+
+namespace Microsoft.Azure.Functions.Worker.Extensions.Tables.TypeConverters
+{
+ ///
+ /// Converter to bind Table client parameter.
+ ///
+ [SupportsDeferredBinding]
+ [SupportedConverterType(typeof(TableClient))]
+ internal class TableClientConverter: TableConverterBase
+ {
+ public TableClientConverter(IOptionsSnapshot tableOptions, ILogger logger)
+ : base(tableOptions, logger)
+ {
+ }
+
+ public override ValueTask ConvertAsync(ConverterContext context)
+ {
+ if (!CanConvert(context))
+ {
+ return new(ConversionResult.Unhandled());
+ }
+ try
+ {
+ var modelBindingData = context?.Source as ModelBindingData;
+ var tableData = GetBindingDataContent(modelBindingData);
+
+ var result = ConvertModelBindingData(tableData);
+
+ if (result is not null)
+ {
+ return new(ConversionResult.Success(result));
+ }
+ }
+ catch (Exception ex)
+ {
+ return new(ConversionResult.Failed(ex));
+ }
+
+ return new(ConversionResult.Unhandled());
+ }
+
+ private TableClient ConvertModelBindingData(TableData content)
+ {
+ if (string.IsNullOrEmpty(content.TableName))
+ {
+ throw new ArgumentNullException(nameof(content.TableName));
+ }
+
+ return GetTableClient(content.Connection, content.TableName!);
+ }
+ }
+}
diff --git a/extensions/Worker.Extensions.Tables/src/TypeConverters/TableConverterBase.cs b/extensions/Worker.Extensions.Tables/src/TypeConverters/TableConverterBase.cs
new file mode 100644
index 000000000..7308f25bb
--- /dev/null
+++ b/extensions/Worker.Extensions.Tables/src/TypeConverters/TableConverterBase.cs
@@ -0,0 +1,80 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Azure.Data.Tables;
+using Microsoft.Azure.Functions.Worker.Converters;
+using Microsoft.Azure.Functions.Worker.Core;
+using Microsoft.Azure.Functions.Worker.Extensions.Tables.Config;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Azure.Functions.Worker.Extensions.Tables.TypeConverters
+{
+ internal abstract class TableConverterBase : IInputConverter
+ {
+ private readonly ILogger> _logger;
+ protected readonly IOptionsSnapshot _tableOptions;
+
+ public TableConverterBase(IOptionsSnapshot tableOptions, ILogger> logger)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _tableOptions = tableOptions ?? throw new ArgumentNullException(nameof(tableOptions));
+ }
+
+ protected bool CanConvert(ConverterContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (context.TargetType != typeof(T))
+ {
+ return false;
+ }
+
+ if (!(context.Source is ModelBindingData bindingData))
+ {
+ return false;
+ }
+
+ if (bindingData.Source is not Constants.TablesExtensionName)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ public abstract ValueTask ConvertAsync(ConverterContext context);
+
+ protected TableData GetBindingDataContent(ModelBindingData? bindingData)
+ {
+ return bindingData?.ContentType switch
+ {
+ Constants.JsonContentType => bindingData.Content.ToObjectFromJson(),
+ _ => throw new NotSupportedException($"Unexpected content-type. Currently only '{Constants.JsonContentType}' is supported.")
+ };
+ }
+
+ protected TableClient GetTableClient(string? connection, string tableName)
+ {
+ var tableOptions = _tableOptions.Get(connection);
+ TableServiceClient tableServiceClient = tableOptions.CreateClient();
+ return tableServiceClient.GetTableClient(tableName);
+ }
+
+ protected class TableData
+ {
+ public string? TableName { get; set; }
+ public string? Connection { get; set; }
+ public string? PartitionKey { get; set; }
+ public string? RowKey { get; set; }
+ public int Take { get; set; }
+ public string? Filter { get; set; }
+ }
+ }
+}
diff --git a/extensions/Worker.Extensions.Tables/src/TypeConverters/TableEntityConverter.cs b/extensions/Worker.Extensions.Tables/src/TypeConverters/TableEntityConverter.cs
new file mode 100644
index 000000000..244e6a903
--- /dev/null
+++ b/extensions/Worker.Extensions.Tables/src/TypeConverters/TableEntityConverter.cs
@@ -0,0 +1,83 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Azure.Data.Tables;
+using Microsoft.Azure.Functions.Worker.Converters;
+using Microsoft.Azure.Functions.Worker.Core;
+using Microsoft.Azure.Functions.Worker.Extensions.Abstractions;
+using Microsoft.Azure.Functions.Worker.Extensions.Tables.Config;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Azure.Functions.Worker.Extensions.Tables.TypeConverters
+{
+ ///
+ /// Converter to bind Table type parameters.
+ ///
+ [SupportsDeferredBinding]
+ [SupportedConverterType(typeof(TableEntity))]
+ internal class TableEntityConverter : TableConverterBase
+ {
+ public TableEntityConverter(IOptionsSnapshot tableOptions, ILogger logger)
+ : base(tableOptions, logger)
+ {
+ }
+
+ public override async ValueTask ConvertAsync(ConverterContext context)
+ {
+ if (!CanConvert(context))
+ {
+ return ConversionResult.Unhandled();
+ }
+
+ try
+ {
+ var modelBindingData = context?.Source as ModelBindingData;
+ var tableData = GetBindingDataContent(modelBindingData);
+
+ var result = await ConvertModelBindingData(tableData);
+
+ if (result is not null)
+ {
+ return ConversionResult.Success(result);
+ }
+
+ }
+ catch (Exception ex)
+ {
+ return ConversionResult.Failed(ex);
+ }
+
+ return ConversionResult.Unhandled();
+ }
+
+ private async Task ConvertModelBindingData(TableData content)
+ {
+ if (string.IsNullOrEmpty(content.TableName))
+ {
+ throw new ArgumentNullException(nameof(content.TableName));
+ }
+
+ if (string.IsNullOrEmpty(content.PartitionKey))
+ {
+ throw new ArgumentNullException(nameof(content.PartitionKey));
+ }
+
+ if (string.IsNullOrEmpty(content.RowKey))
+ {
+ throw new ArgumentNullException(nameof(content.RowKey));
+ }
+
+ return await GetTableEntity(content);
+ }
+
+ private async Task GetTableEntity(TableData content)
+ {
+ var tableClient = GetTableClient(content.Connection, content.TableName!);
+ return await tableClient.GetEntityAsync(content.PartitionKey, content.RowKey);
+ }
+ }
+}
diff --git a/extensions/Worker.Extensions.Tables/src/TypeConverters/TableEntityEnumerableConverter.cs b/extensions/Worker.Extensions.Tables/src/TypeConverters/TableEntityEnumerableConverter.cs
new file mode 100644
index 000000000..0479fdaa1
--- /dev/null
+++ b/extensions/Worker.Extensions.Tables/src/TypeConverters/TableEntityEnumerableConverter.cs
@@ -0,0 +1,108 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Azure.Data.Tables;
+using Microsoft.Azure.Functions.Worker.Converters;
+using Microsoft.Azure.Functions.Worker.Core;
+using Microsoft.Azure.Functions.Worker.Extensions.Abstractions;
+using Microsoft.Azure.Functions.Worker.Extensions.Tables.Config;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Azure.Functions.Worker.Extensions.Tables.TypeConverters
+{
+ ///
+ /// Converter to bind Table type parameters.
+ ///
+ [SupportsDeferredBinding]
+ [SupportedConverterType(typeof(IEnumerable))]
+ internal class TableEntityEnumerableConverter : TableConverterBase>
+ {
+ public TableEntityEnumerableConverter(IOptionsSnapshot tableOptions, ILogger logger)
+ : base(tableOptions, logger)
+ {
+ }
+
+ public override async ValueTask ConvertAsync(ConverterContext context)
+ {
+ if (!CanConvert(context))
+ {
+ return ConversionResult.Unhandled();
+ }
+ try
+ {
+ var modelBindingData = context?.Source as ModelBindingData;
+ var tableData = GetBindingDataContent(modelBindingData);
+
+ var result = await ConvertModelBindingData(tableData);
+
+ if (result is not null)
+ {
+ return ConversionResult.Success(result);
+ }
+ }
+ catch (Exception ex)
+ {
+ return ConversionResult.Failed(ex);
+ }
+
+ return ConversionResult.Unhandled();
+ }
+
+ private async Task> ConvertModelBindingData(TableData content)
+ {
+ if (string.IsNullOrEmpty(content.TableName))
+ {
+ throw new ArgumentNullException(nameof(content.TableName));
+ }
+
+ if (content.RowKey != null && (content.Take > 0 || content.Filter != null))
+ {
+ throw new InvalidOperationException($"Row key {content.RowKey} cannot have a value if {content.Take} or {content.Filter} are defined");
+ }
+
+ return await GetEnumerableTableEntity(content);
+ }
+
+ private async Task> GetEnumerableTableEntity(TableData content)
+ {
+ var tableClient = GetTableClient(content.Connection, content.TableName!);
+ string? filter = content.Filter;
+
+ if (!string.IsNullOrEmpty(content.PartitionKey))
+ {
+ var partitionKeyPredicate = TableClient.CreateQueryFilter($"PartitionKey eq {content.PartitionKey}");
+ filter = !string.IsNullOrEmpty(content.Filter) ? $"{partitionKeyPredicate} and {content.Filter}" : partitionKeyPredicate;
+ }
+
+ int? maxPerPage = null;
+ if (content.Take > 0)
+ {
+ maxPerPage = content.Take;
+ }
+
+ int countRemaining = content.Take;
+
+ var entities = tableClient.QueryAsync(
+ filter: filter,
+ maxPerPage: maxPerPage).ConfigureAwait(false);
+
+ List bindingDataContent = new List();
+
+ await foreach (var entity in entities)
+ {
+ countRemaining--;
+ bindingDataContent.Add(entity);
+ if (countRemaining == 0)
+ {
+ break;
+ }
+ }
+ return bindingDataContent;
+ }
+ }
+}
+
diff --git a/extensions/Worker.Extensions.Tables/src/Worker.Extensions.Tables.csproj b/extensions/Worker.Extensions.Tables/src/Worker.Extensions.Tables.csproj
index 2868a547c..6ad057dc0 100644
--- a/extensions/Worker.Extensions.Tables/src/Worker.Extensions.Tables.csproj
+++ b/extensions/Worker.Extensions.Tables/src/Worker.Extensions.Tables.csproj
@@ -6,7 +6,8 @@
Azure Table Storage extensions for .NET isolated functions
- 1.0.0
+ 1.2.0
+ -preview1
@@ -14,6 +15,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/WorkerBindingSamples/NuGet.Config b/samples/WorkerBindingSamples/NuGet.Config
deleted file mode 100644
index 3f0e00340..000000000
--- a/samples/WorkerBindingSamples/NuGet.Config
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/samples/WorkerBindingSamples/Table/TableInputBindingSamples.cs b/samples/WorkerBindingSamples/Table/TableInputBindingSamples.cs
new file mode 100644
index 000000000..cd7cc27f8
--- /dev/null
+++ b/samples/WorkerBindingSamples/Table/TableInputBindingSamples.cs
@@ -0,0 +1,118 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System.Net;
+using Azure.Data.Tables;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Http;
+using Microsoft.Extensions.Logging;
+
+namespace WorkerBindingSamples.Table
+{
+ public class TableInputBindingSamples
+ {
+ private readonly ILogger _logger;
+
+ public TableInputBindingSamples(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ [Function(nameof(TableClientFunction))]
+ public async Task TableClientFunction(
+ [HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req,
+ [TableInput("TableName")] TableClient table)
+ {
+ var tableEntity = table.QueryAsync();
+ var response = req.CreateResponse(HttpStatusCode.OK);
+
+ await foreach (TableEntity val in tableEntity)
+ {
+ val.TryGetValue("Text", out var text);
+ _logger.LogInformation("Value of text: " + text);
+
+ await response.WriteStringAsync(text?.ToString() ?? "");
+ }
+
+ return response;
+ }
+
+ [Function(nameof(ReadTableDataFunction))]
+ public async Task ReadTableDataFunction(
+ [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "items/{partitionKey}/{rowKey}")] HttpRequestData req,
+ [TableInput("TableName", "{partitionKey}", "{rowKey}")] TableEntity table)
+
+ {
+ table.TryGetValue("Text", out var text);
+ var response = req.CreateResponse(HttpStatusCode.OK);
+ await response.WriteStringAsync(text?.ToString() ?? "");
+ return response;
+ }
+
+ [Function(nameof(ReadTableDataFunctionWithFilter))]
+ public async Task ReadTableDataFunctionWithFilter(
+ [HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req,
+ [TableInput("TableName", "My Partition", 2, Filter = "RowKey ne 'value'")] IEnumerable table)
+
+ {
+ List tableList = new();
+ var response = req.CreateResponse(HttpStatusCode.OK);
+
+ foreach (TableEntity tableEntity in table)
+ {
+ tableEntity.TryGetValue("Text", out var text);
+ _logger.LogInformation("Value of text: " + text);
+ tableList.Add(text?.ToString() ?? "");
+ }
+
+ await response.WriteStringAsync(string.Join(",", tableList));
+ return response;
+ }
+
+ [Function(nameof(EnumerableFunction))]
+ public async Task EnumerableFunction(
+ [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "items/{partitionKey}")] HttpRequestData req,
+ [TableInput("TableName", "{partitionKey}")] IEnumerable tables)
+
+ {
+ var response = req.CreateResponse(HttpStatusCode.OK);
+ List tableList = new();
+
+ foreach (TableEntity tableEntity in tables)
+ {
+ tableEntity.TryGetValue("Text", out var text);
+ _logger.LogInformation("Value of text: " + text);
+ tableList.Add((text?.ToString()) ?? "");
+ }
+
+ await response.WriteStringAsync(string.Join(",", tableList));
+ return response;
+ }
+
+ [Function(nameof(PocoFunction))]
+ public async Task PocoFunction(
+ [HttpTrigger(AuthorizationLevel.Function, "get","post", Route = null)] HttpRequestData req,
+ [TableInput("TableName")] IEnumerable entities,
+ FunctionContext executionContext)
+ {
+ var response = req.CreateResponse(HttpStatusCode.OK);
+ List entityList = new();
+
+ foreach (MyEntity entity in entities)
+ {
+ _logger.LogInformation($"Text: {entity.Text}");
+ entityList.Add((entity.Text ?? "").ToString());
+ }
+
+ await response.WriteStringAsync(string.Join(",", entityList));
+ return response;
+ }
+ }
+
+ public class MyEntity
+ {
+ public string? Text { get; set; }
+ public string? PartitionKey { get; set; }
+ public string? RowKey { get; set; }
+ }
+}
diff --git a/samples/WorkerBindingSamples/WorkerBindingSamples.csproj b/samples/WorkerBindingSamples/WorkerBindingSamples.csproj
index 5e4c33b89..b804f5429 100644
--- a/samples/WorkerBindingSamples/WorkerBindingSamples.csproj
+++ b/samples/WorkerBindingSamples/WorkerBindingSamples.csproj
@@ -7,11 +7,12 @@
enable
-
+
+
diff --git a/test/DotNetWorkerTests/DotNetWorkerTests.csproj b/test/DotNetWorkerTests/DotNetWorkerTests.csproj
index 4fb9cfa48..0b7cf4c2a 100644
--- a/test/DotNetWorkerTests/DotNetWorkerTests.csproj
+++ b/test/DotNetWorkerTests/DotNetWorkerTests.csproj
@@ -26,6 +26,7 @@
+
diff --git a/test/DotNetWorkerTests/GrpcFunctionDefinitionTests.cs b/test/DotNetWorkerTests/GrpcFunctionDefinitionTests.cs
index 34dc65348..72d56e561 100644
--- a/test/DotNetWorkerTests/GrpcFunctionDefinitionTests.cs
+++ b/test/DotNetWorkerTests/GrpcFunctionDefinitionTests.cs
@@ -6,6 +6,7 @@
using System.IO;
using System.Linq;
using System.Threading;
+using Azure.Data.Tables;
using Microsoft.Azure.Functions.Tests;
using Microsoft.Azure.Functions.Worker.Converters;
using Microsoft.Azure.Functions.Worker.Core;
@@ -146,6 +147,73 @@ public void GrpcFunctionDefinition_BlobInput_Creates()
});
}
+ [Fact]
+ public void GrpcFunctionDefinition_TableInput_Creates()
+ {
+ using var testVariables = new TestScopedEnvironmentVariable("FUNCTIONS_WORKER_DIRECTORY", ".");
+
+ var bindingInfoProvider = new DefaultOutputBindingsInfoProvider();
+ var methodInfoLocator = new DefaultMethodInfoLocator();
+
+ string fullPathToThisAssembly = GetType().Assembly.Location;
+ var functionLoadRequest = new FunctionLoadRequest
+ {
+ FunctionId = "abc",
+ Metadata = new RpcFunctionMetadata
+ {
+ EntryPoint = $"Microsoft.Azure.Functions.Worker.Tests.{nameof(GrpcFunctionDefinitionTests)}+{nameof(MyTableFunctionClass)}.{nameof(MyTableFunctionClass.Run)}",
+ ScriptFile = Path.GetFileName(fullPathToThisAssembly),
+ Name = "myfunction"
+ }
+ };
+
+ // We base this on the request exclusively, not the binding attributes.
+ functionLoadRequest.Metadata.Bindings.Add("req", new BindingInfo { Type = "HttpTrigger", Direction = Direction.In });
+ functionLoadRequest.Metadata.Bindings.Add("$return", new BindingInfo { Type = "Http", Direction = Direction.Out });
+
+ FunctionDefinition definition = functionLoadRequest.ToFunctionDefinition(methodInfoLocator);
+
+ Assert.Equal(functionLoadRequest.FunctionId, definition.Id);
+ Assert.Equal(functionLoadRequest.Metadata.EntryPoint, definition.EntryPoint);
+ Assert.Equal(functionLoadRequest.Metadata.Name, definition.Name);
+ Assert.Equal(fullPathToThisAssembly, definition.PathToAssembly);
+
+ // Parameters
+ Assert.Collection(definition.Parameters,
+ p =>
+ {
+ Assert.Equal("req", p.Name);
+ Assert.Equal(typeof(HttpRequestData), p.Type);
+ },
+ q =>
+ {
+ Assert.Equal("tableInput", q.Name);
+ Assert.Equal(typeof(TableClient), q.Type);
+ Assert.Contains(PropertyBagKeys.AllowConverterFallback, q.Properties.Keys);
+ Assert.Contains(PropertyBagKeys.BindingAttributeSupportedConverters, q.Properties.Keys);
+ Assert.True(true, q.Properties[PropertyBagKeys.AllowConverterFallback].ToString());
+ Assert.Contains(new Dictionary>().ToString(), q.Properties[PropertyBagKeys.BindingAttributeSupportedConverters].ToString());
+ });
+
+ // InputBindings
+ Assert.Collection(definition.InputBindings,
+ p =>
+ {
+ Assert.Equal("req", p.Key);
+ Assert.Equal(BindingDirection.In, p.Value.Direction);
+ Assert.Equal("HttpTrigger", p.Value.Type);
+ });
+
+ // OutputBindings
+ Assert.Collection(definition.OutputBindings,
+ p =>
+ {
+ Assert.Equal("$return", p.Key);
+ Assert.Equal(BindingDirection.Out, p.Value.Direction);
+ Assert.Equal("Http", p.Value.Type);
+ });
+ }
+
private class MyFunctionClass
{
@@ -173,5 +241,14 @@ public HttpResponseData Run(
}
}
+ private class MyTableFunctionClass
+ {
+ public HttpResponseData Run(
+ [HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req,
+ [TableInput("tableName")] TableClient tableInput)
+ {
+ return req.CreateResponse();
+ }
+ }
}
}
diff --git a/test/E2ETests/E2EApps/E2EApp/E2EApp.csproj b/test/E2ETests/E2EApps/E2EApp/E2EApp.csproj
index efcb5a6fa..218be1a05 100644
--- a/test/E2ETests/E2EApps/E2EApp/E2EApp.csproj
+++ b/test/E2ETests/E2EApps/E2EApp/E2EApp.csproj
@@ -25,6 +25,7 @@
+
@@ -44,7 +45,7 @@
-
+
diff --git a/test/E2ETests/E2EApps/E2EApp/Table/TableInputBindingFunctions.cs b/test/E2ETests/E2EApps/E2EApp/Table/TableInputBindingFunctions.cs
new file mode 100644
index 000000000..d1b2ac632
--- /dev/null
+++ b/test/E2ETests/E2EApps/E2EApp/Table/TableInputBindingFunctions.cs
@@ -0,0 +1,109 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.Net;
+using System.Threading.Tasks;
+using Azure.Data.Tables;
+using Microsoft.Azure.Functions.Worker.Http;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Azure.Functions.Worker.E2EApp.Table
+{
+ public class TableInputBindingFunction
+ {
+ private readonly ILogger _logger;
+
+ public TableInputBindingFunction(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ [Function(nameof(TableClientFunction))]
+ public async Task TableClientFunction(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
+ [TableInput("TableName")] TableClient table)
+ {
+ var tableEntity = table.QueryAsync();
+ var response = req.CreateResponse(HttpStatusCode.OK);
+
+ await foreach (TableEntity val in tableEntity)
+ {
+ val.TryGetValue("Text", out var text);
+
+ await response.WriteStringAsync(text?.ToString() ?? "");
+ }
+
+ return response;
+ }
+
+
+ [Function(nameof(ReadTableDataFunction))]
+ public async Task ReadTableDataFunction(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "ReadTableDataFunction/items/{partitionKey}/{rowKey}")] HttpRequestData req,
+ [TableInput("TableName", "{partitionKey}", "{rowKey}")] TableEntity table)
+
+ {
+ table.TryGetValue("Text", out var text);
+ var response = req.CreateResponse(HttpStatusCode.OK);
+ await response.WriteStringAsync(text?.ToString() ?? "");
+ return response;
+ }
+
+ [Function(nameof(ReadTableDataFunctionWithFilter))]
+ public async Task ReadTableDataFunctionWithFilter(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "ReadTableDataFunctionWithFilter/items/{partition}/{rowKey}")] HttpRequestData req,
+ [TableInput("TableName", "{partition}", 2, Filter = "RowKey ne '" + "{rowKey}'")] IEnumerable table)
+ {
+ List tableList = new();
+ var response = req.CreateResponse(HttpStatusCode.OK);
+
+ foreach (TableEntity tableEntity in table)
+ {
+ tableEntity.TryGetValue("Text", out var text);
+ _logger.LogInformation("Value of text: " + text);
+ tableList.Add(text?.ToString() ?? "");
+ }
+
+ await response.WriteStringAsync(string.Join(",", tableList));
+ return response;
+ }
+
+ [Function(nameof(EnumerableFunction))]
+ public async Task EnumerableFunction(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "EnumerableFunction/items/{partitionKey}")] HttpRequestData req,
+ [TableInput("TableName", "{partitionKey}")] IEnumerable tables)
+
+ {
+ var response = req.CreateResponse(HttpStatusCode.OK);
+ List tableList = new();
+
+ foreach (TableEntity tableEntity in tables)
+ {
+ tableEntity.TryGetValue("Text", out var text);
+ _logger.LogInformation("Value of text: " + text);
+ tableList.Add(text?.ToString() ?? "");
+ }
+
+ await response.WriteStringAsync(string.Join(",", tableList));
+ return response;
+ }
+
+ public class MyPoco
+ {
+ public string PartitionKey { get; set; }
+ public string RowKey { get; set; }
+ public string Text { get; set; }
+ }
+
+ [Function("DoesNotSupportDeferredBinding")]
+ public static void TableInput(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
+ [TableInput("MyTable", "MyPartition", "yo")] MyPoco poco,
+ ILogger log)
+ {
+ log.LogInformation($"PK={poco.PartitionKey}, RK={poco.RowKey}, Text={poco.Text}");
+ }
+ }
+}
+
diff --git a/test/E2ETests/E2ETests/Constants.cs b/test/E2ETests/E2ETests/Constants.cs
index f9cd316f0..86648e130 100644
--- a/test/E2ETests/E2ETests/Constants.cs
+++ b/test/E2ETests/E2ETests/Constants.cs
@@ -86,5 +86,13 @@ public static class Cardinality_One_Test
// Xunit Fixtures and Collections
public const string FunctionAppCollectionName = "FunctionAppCollection";
+
+ // Tables tests
+ public static class Tables
+ {
+ public const string EmulatorConnectionString = "UseDevelopmentStorage=true";
+ public const string TablesConnectionStringSetting = EmulatorConnectionString;
+ public const string TableName = "TableName";
+ }
}
}
diff --git a/test/E2ETests/E2ETests/E2ETests.csproj b/test/E2ETests/E2ETests/E2ETests.csproj
index 56fad6d55..f7cbb5240 100644
--- a/test/E2ETests/E2ETests/E2ETests.csproj
+++ b/test/E2ETests/E2ETests/E2ETests.csproj
@@ -19,6 +19,7 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/test/E2ETests/E2ETests/Helpers/TableHelpers.cs b/test/E2ETests/E2ETests/Helpers/TableHelpers.cs
new file mode 100644
index 000000000..b37055e79
--- /dev/null
+++ b/test/E2ETests/E2ETests/Helpers/TableHelpers.cs
@@ -0,0 +1,33 @@
+using System.Threading.Tasks;
+using Azure.Data.Tables;
+using Microsoft.Azure.Functions.Tests.E2ETests;
+
+namespace Microsoft.Azure.Functions.Worker.E2ETests.Helpers
+{
+ public static class TableHelpers
+ {
+ private static readonly TableClient _tableClient;
+ static TableHelpers()
+ {
+ var tableName = Constants.Tables.TableName;
+ _tableClient = new TableClient(Constants.Tables.TablesConnectionStringSetting, tableName);
+ }
+
+ public async static Task CreateTable()
+ {
+ await _tableClient.CreateIfNotExistsAsync();
+ }
+
+ public async static Task DeleteTable()
+ {
+ await _tableClient.DeleteAsync();
+ }
+
+ public async static Task CreateTableEntity(string partitionKey, string rowKey, string value)
+ {
+ var tableEntity = new TableEntity(partitionKey, rowKey);
+ tableEntity.Add("Text", value);
+ await _tableClient.AddEntityAsync(tableEntity);
+ }
+ }
+}
diff --git a/test/E2ETests/E2ETests/Tables/TablesEndToEndTests.cs b/test/E2ETests/E2ETests/Tables/TablesEndToEndTests.cs
new file mode 100644
index 000000000..79610cb5c
--- /dev/null
+++ b/test/E2ETests/E2ETests/Tables/TablesEndToEndTests.cs
@@ -0,0 +1,150 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.Azure.Functions.Worker.E2ETests.Helpers;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.Azure.Functions.Tests.E2ETests.Tables
+{
+ [Collection(Constants.FunctionAppCollectionName)]
+ public class TablesEndToEndTests : IDisposable
+ {
+ private readonly IDisposable _disposeLog;
+ private FunctionAppFixture _fixture;
+
+ public TablesEndToEndTests(FunctionAppFixture fixture, ITestOutputHelper testOutput)
+ {
+ _fixture = fixture;
+ _disposeLog = _fixture.TestLogs.UseTestLogger(testOutput);
+ }
+
+ [Fact]
+ public async Task Read_TableClient_Data_Succeeds()
+ {
+ const string partitionKey = "Partition";
+ const string firstRowKey = "FirstRowKey";
+ const string firstValue = "FirstValue";
+
+ // Create the table if it doesn't exist
+ await TableHelpers.CreateTable();
+
+ // Add table entity
+ await TableHelpers.CreateTableEntity(partitionKey, firstRowKey, firstValue);
+
+ //Trigger
+ HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger("TableClientFunction");
+ string actualMessage = await response.Content.ReadAsStringAsync();
+
+ //Verify
+ HttpStatusCode expectedStatusCode = HttpStatusCode.OK;
+
+ Assert.Equal(expectedStatusCode, response.StatusCode);
+ Assert.Equal(firstValue, actualMessage);
+
+ // Delete table
+ await TableHelpers.DeleteTable();
+ }
+
+ [Fact]
+ public async Task Read_TableData_Succeeds()
+ {
+ const string partitionKey = "Partition";
+ const string firstRowKey = "FirstRowKey";
+ const string firstValue = "FirstValue";
+
+ // Create the table if it doesn't exist
+ await TableHelpers.CreateTable();
+
+ // Add table entity
+ await TableHelpers.CreateTableEntity(partitionKey, firstRowKey, firstValue);
+
+ //Trigger
+ HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger($"ReadTableDataFunction/items/{partitionKey}/{firstRowKey}");
+ string actualMessage = await response.Content.ReadAsStringAsync();
+
+ //Verify
+ HttpStatusCode expectedStatusCode = HttpStatusCode.OK;
+
+ Assert.Equal(expectedStatusCode, response.StatusCode);
+ Assert.Equal(firstValue, actualMessage);
+
+ // Delete table
+ await TableHelpers.DeleteTable();
+ }
+
+ [Fact]
+ public async Task Read_TableData_With_Filter_And_Take()
+ {
+ const string partitionKey = "Partition";
+ const string firstRowKey = "FirstRowKey";
+ const string firstValue = "FirstValue";
+ const string secondRowKey = "SecondRowKey";
+ const string secondValue = "SecondValue";
+ const string thirdRowKey = "ThirdRowKey";
+ const string thirdValue = "ThirdValue";
+
+ // Create table
+ await TableHelpers.CreateTable();
+
+ // Add table entity
+ await TableHelpers.CreateTableEntity(partitionKey, firstRowKey, firstValue);
+ await TableHelpers.CreateTableEntity(partitionKey, secondRowKey, secondValue);
+ await TableHelpers.CreateTableEntity(partitionKey, thirdRowKey, thirdValue);
+
+ //Trigger
+ HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger($"ReadTableDataFunctionWithFilter/items/{partitionKey}/{secondRowKey}");
+ string actualMessage = await response.Content.ReadAsStringAsync();
+
+ //Verify
+ HttpStatusCode expectedStatusCode = HttpStatusCode.OK;
+
+ Assert.Equal(expectedStatusCode, response.StatusCode);
+ Assert.Equal($"{firstValue},{thirdValue}", actualMessage);
+
+ // Delete table
+ await TableHelpers.DeleteTable();
+ }
+
+ [Fact]
+ public async Task EnumerableFunction_Succeeds()
+ {
+ const string partitionKey = "Partition";
+ const string firstRowKey = "FirstRowKey";
+ const string firstValue = "FirstValue";
+ const string secondRowKey = "SecondRowKey";
+ const string secondValue = "SecondValue";
+
+ // Create table
+ await TableHelpers.CreateTable();
+
+ // Add table entity
+ await TableHelpers.CreateTableEntity(partitionKey, firstRowKey, firstValue);
+ await TableHelpers.CreateTableEntity(partitionKey, secondRowKey, secondValue);
+
+ //Trigger
+ HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger($"EnumerableFunction/items/{partitionKey}");
+ string actualMessage = await response.Content.ReadAsStringAsync();
+
+ //Verify
+ HttpStatusCode expectedStatusCode = HttpStatusCode.OK;
+
+ Assert.Equal(expectedStatusCode, response.StatusCode);
+ Assert.Equal($"{firstValue},{secondValue}", actualMessage);
+
+ // Delete table
+ await TableHelpers.DeleteTable();
+ }
+
+ public void Dispose()
+ {
+ // Cleanup
+ TableHelpers.DeleteTable().GetAwaiter().GetResult();
+ _disposeLog?.Dispose();
+ }
+ }
+}
diff --git a/test/FunctionMetadataGeneratorTests/FunctionMetadataGeneratorTests.cs b/test/FunctionMetadataGeneratorTests/FunctionMetadataGeneratorTests.cs
index a81889457..c0ea88c28 100644
--- a/test/FunctionMetadataGeneratorTests/FunctionMetadataGeneratorTests.cs
+++ b/test/FunctionMetadataGeneratorTests/FunctionMetadataGeneratorTests.cs
@@ -10,6 +10,7 @@
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
+using Azure.Data.Tables;
using Azure.Messaging.ServiceBus;
using Azure.Storage.Blobs;
using Microsoft.Azure.Functions.Tests;
@@ -264,7 +265,7 @@ public void BlobStorageFunctions_SDKTypeBindings()
b => ValidateBlobInput(b),
b => ValidateBlobOutput(b));
-
+
AssertDictionary(extensions, new Dictionary
{
{ "Microsoft.Azure.WebJobs.Extensions.Storage.Blobs", "5.1.1" },
@@ -440,6 +441,75 @@ void ValidateBlobOutput(ExpandoObject b)
}
}
+ [Fact]
+ public void TableFunctions_SDKTypeBindings()
+ {
+ var generator = new FunctionMetadataGenerator();
+ var module = ModuleDefinition.ReadModule(_thisAssembly.Location);
+ var typeDef = TestUtility.GetTypeDefinition(typeof(SDKTypeBindings_Table));
+ var functions = generator.GenerateFunctionMetadata(typeDef);
+ var extensions = generator.Extensions;
+
+ Assert.Equal(5, functions.Count());
+
+ var tableClientFunction = functions.Single(p => p.Name == "TableClientFunction");
+
+ ValidateFunction(tableClientFunction, "TableClientFunction", GetEntryPoint(nameof(SDKTypeBindings_Table), nameof(SDKTypeBindings_Table.TableClientFunction)),
+ b => ValidateTableInput(b));
+
+ AssertDictionary(extensions, new Dictionary
+ {
+ { "Microsoft.Azure.WebJobs.Extensions.Tables", "1.2.0-beta.1" },
+ });
+
+ var tableEntityFunction = functions.Single(p => p.Name == "TableEntityFunction");
+
+ ValidateFunction(tableEntityFunction, "TableEntityFunction", GetEntryPoint(nameof(SDKTypeBindings_Table), nameof(SDKTypeBindings_Table.TableEntityFunction)),
+ b => ValidateTableInput(b));
+
+
+ var enumerableTableEntityFunction = functions.Single(p => p.Name == "EnumerableTableEntityFunction");
+
+ ValidateFunction(enumerableTableEntityFunction, "EnumerableTableEntityFunction", GetEntryPoint(nameof(SDKTypeBindings_Table), nameof(SDKTypeBindings_Table.EnumerableTableEntityFunction)),
+ b => ValidateTableInput(b));
+
+
+ var tableUnsupportedTypeFunction = functions.Single(p => p.Name == "TableUnsupportedTypeFunction");
+
+ ValidateFunction(tableUnsupportedTypeFunction, "TableUnsupportedTypeFunction", GetEntryPoint(nameof(SDKTypeBindings_Table), nameof(SDKTypeBindings_Table.TableUnsupportedTypeFunction)),
+ b => ValidateTableInputBypassDeferredBinding(b));
+
+
+ var tablePocoFunction = functions.Single(p => p.Name == "TablePocoFunction");
+
+ ValidateFunction(tablePocoFunction, "TablePocoFunction", GetEntryPoint(nameof(SDKTypeBindings_Table), nameof(SDKTypeBindings_Table.TablePocoFunction)),
+ b => ValidateTableInputBypassDeferredBinding(b));
+
+ void ValidateTableInput(ExpandoObject b)
+ {
+ AssertExpandoObject(b, new Dictionary
+ {
+ { "Name", "tableInput" },
+ { "Type", "table" },
+ { "Direction", "In" },
+ { "tableName", "tableName" },
+ { "Properties", new Dictionary( ) { { "SupportsDeferredBinding" , "True"} } }
+ });
+ }
+
+ void ValidateTableInputBypassDeferredBinding(ExpandoObject b)
+ {
+ AssertExpandoObject(b, new Dictionary
+ {
+ { "Name", "tableInput" },
+ { "Type", "table" },
+ { "Direction", "In" },
+ { "tableName", "tableName" },
+ { "Properties", new Dictionary( ) { } }
+ });
+ }
+ }
+
[Fact]
public void TimerFunction()
{
@@ -1009,7 +1079,6 @@ public object BlobStringToBlobStringFunction(
throw new NotImplementedException();
}
-
[Function("BlobClientToBlobStringFunction")]
[BlobOutput("container1/hello.txt", Connection = "MyOtherConnection")]
public object BlobClientToBlobStreamFunction(
@@ -1047,6 +1116,46 @@ public object BlobPocoToBlobUnsupportedType(
}
}
+ private class SDKTypeBindings_Table
+ {
+ [Function("TableClientFunction")]
+ public object TableClientFunction(
+ [TableInput("tableName")] TableClient tableInput)
+ {
+ throw new NotImplementedException();
+ }
+
+
+ [Function("TableEntityFunction")]
+ public object TableEntityFunction(
+ [TableInput("tableName")] TableEntity tableInput)
+ {
+ throw new NotImplementedException();
+ }
+
+
+ [Function("EnumerableTableEntityFunction")]
+ public object EnumerableTableEntityFunction(
+ [TableInput("tableName")] IEnumerable tableInput)
+ {
+ throw new NotImplementedException();
+ }
+
+ [Function("TableUnsupportedTypeFunction")]
+ public object TableUnsupportedTypeFunction(
+ [TableInput("tableName")] BinaryData tableInput)
+ {
+ throw new NotImplementedException();
+ }
+
+ [Function("TablePocoFunction")]
+ public object TablePocoFunction(
+ [TableInput("tableName")] Poco tableInput)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
private class SDKTypeBindings_BlobCollection
{
[Function("BlobStringToBlobStringArray")]
diff --git a/test/FunctionMetadataGeneratorTests/SdkTests.csproj b/test/FunctionMetadataGeneratorTests/SdkTests.csproj
index 4fb4cbe92..c0728f5ed 100644
--- a/test/FunctionMetadataGeneratorTests/SdkTests.csproj
+++ b/test/FunctionMetadataGeneratorTests/SdkTests.csproj
@@ -11,6 +11,7 @@
+
@@ -23,7 +24,7 @@
-
+
@@ -34,6 +35,7 @@
+
diff --git a/test/SdkE2ETests/Contents/WorkerBindingSamplesOutput/functions.metadata b/test/SdkE2ETests/Contents/WorkerBindingSamplesOutput/functions.metadata
index 7b34cc393..eddb53b95 100644
--- a/test/SdkE2ETests/Contents/WorkerBindingSamplesOutput/functions.metadata
+++ b/test/SdkE2ETests/Contents/WorkerBindingSamplesOutput/functions.metadata
@@ -1,4 +1,190 @@
[
+ {
+ "name": "TableClientFunction",
+ "scriptFile": "WorkerBindingSamples.dll",
+ "entryPoint": "WorkerBindingSamples.Table.TableInputBindingSamples.TableClientFunction",
+ "language": "dotnet-isolated",
+ "properties": {
+ "IsCodeless": false
+ },
+ "bindings": [
+ {
+ "name": "req",
+ "direction": "In",
+ "type": "httpTrigger",
+ "authLevel": "Function",
+ "methods": [
+ "get",
+ "post"
+ ],
+ "properties": {}
+ },
+ {
+ "name": "table",
+ "direction": "In",
+ "type": "table",
+ "tableName": "TableName",
+ "properties": {
+ "supportsDeferredBinding": "True"
+ }
+ },
+ {
+ "name": "$return",
+ "type": "http",
+ "direction": "Out"
+ }
+ ]
+ },
+ {
+ "name": "ReadTableDataFunction",
+ "scriptFile": "WorkerBindingSamples.dll",
+ "entryPoint": "WorkerBindingSamples.Table.TableInputBindingSamples.ReadTableDataFunction",
+ "language": "dotnet-isolated",
+ "properties": {
+ "IsCodeless": false
+ },
+ "bindings": [
+ {
+ "name": "req",
+ "direction": "In",
+ "type": "httpTrigger",
+ "authLevel": "Function",
+ "methods": [
+ "get",
+ "post"
+ ],
+ "route": "items/{partitionKey}/{rowKey}",
+ "properties": {}
+ },
+ {
+ "name": "table",
+ "direction": "In",
+ "type": "table",
+ "tableName": "TableName",
+ "partitionKey": "{partitionKey}",
+ "rowKey": "{rowKey}",
+ "properties": {
+ "supportsDeferredBinding": "True"
+ }
+ },
+ {
+ "name": "$return",
+ "type": "http",
+ "direction": "Out"
+ }
+ ]
+ },
+ {
+ "name": "ReadTableDataFunctionWithFilter",
+ "scriptFile": "WorkerBindingSamples.dll",
+ "entryPoint": "WorkerBindingSamples.Table.TableInputBindingSamples.ReadTableDataFunctionWithFilter",
+ "language": "dotnet-isolated",
+ "properties": {
+ "IsCodeless": false
+ },
+ "bindings": [
+ {
+ "name": "req",
+ "direction": "In",
+ "type": "httpTrigger",
+ "authLevel": "Function",
+ "methods": [
+ "get",
+ "post"
+ ],
+ "properties": {}
+ },
+ {
+ "name": "table",
+ "direction": "In",
+ "type": "table",
+ "tableName": "TableName",
+ "partitionKey": "My Partition",
+ "take": 2,
+ "filter": "RowKey ne 'value'",
+ "properties": {
+ "supportsDeferredBinding": "True"
+ }
+ },
+ {
+ "name": "$return",
+ "type": "http",
+ "direction": "Out"
+ }
+ ]
+ },
+ {
+ "name": "EnumerableFunction",
+ "scriptFile": "WorkerBindingSamples.dll",
+ "entryPoint": "WorkerBindingSamples.Table.TableInputBindingSamples.EnumerableFunction",
+ "language": "dotnet-isolated",
+ "properties": {
+ "IsCodeless": false
+ },
+ "bindings": [
+ {
+ "name": "req",
+ "direction": "In",
+ "type": "httpTrigger",
+ "authLevel": "Function",
+ "methods": [
+ "get",
+ "post"
+ ],
+ "route": "items/{partitionKey}",
+ "properties": {}
+ },
+ {
+ "name": "tables",
+ "direction": "In",
+ "type": "table",
+ "tableName": "TableName",
+ "partitionKey": "{partitionKey}",
+ "properties": {
+ "supportsDeferredBinding": "True"
+ }
+ },
+ {
+ "name": "$return",
+ "type": "http",
+ "direction": "Out"
+ }
+ ]
+ },
+ {
+ "name": "PocoFunction",
+ "scriptFile": "WorkerBindingSamples.dll",
+ "entryPoint": "WorkerBindingSamples.Table.TableInputBindingSamples.PocoFunction",
+ "language": "dotnet-isolated",
+ "properties": {
+ "IsCodeless": false
+ },
+ "bindings": [
+ {
+ "name": "req",
+ "direction": "In",
+ "type": "httpTrigger",
+ "authLevel": "Function",
+ "methods": [
+ "get",
+ "post"
+ ],
+ "properties": {}
+ },
+ {
+ "name": "entities",
+ "direction": "In",
+ "type": "table",
+ "tableName": "TableName",
+ "properties": {}
+ },
+ {
+ "name": "$return",
+ "type": "http",
+ "direction": "Out"
+ }
+ ]
+ },
{
"name": "BlobInputClientFunction",
"scriptFile": "WorkerBindingSamples.dll",
diff --git a/test/SdkE2ETests/PublishTests.cs b/test/SdkE2ETests/PublishTests.cs
index 251b8b3f4..58c37e015 100644
--- a/test/SdkE2ETests/PublishTests.cs
+++ b/test/SdkE2ETests/PublishTests.cs
@@ -134,7 +134,10 @@ private async Task RunPublishTestForSdkTypeBindings(string outputDir, string add
@"./.azurefunctions/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs.dll"),
new Extension("AzureStorageQueues",
"Microsoft.Azure.WebJobs.Extensions.Storage.AzureStorageQueuesWebJobsStartup, Microsoft.Azure.WebJobs.Extensions.Storage.Queues, Version=5.0.0.0, Culture=neutral, PublicKeyToken=92742159e12e44c8",
- @"./.azurefunctions/Microsoft.Azure.WebJobs.Extensions.Storage.Queues.dll")
+ @"./.azurefunctions/Microsoft.Azure.WebJobs.Extensions.Storage.Queues.dll"),
+ new Extension("AzureTables",
+ "Microsoft.Azure.WebJobs.Extensions.Tables.AzureTablesWebJobsStartup, Microsoft.Azure.WebJobs.Extensions.Tables, Version=1.2.0.0, Culture=neutral, PublicKeyToken=92742159e12e44c8",
+ @"./.azurefunctions/Microsoft.Azure.WebJobs.Extensions.Tables.dll")
}
});
Assert.True(JToken.DeepEquals(extensionsJsonContents, expected), $"Actual: {extensionsJsonContents}{Environment.NewLine}Expected: {expected}");
diff --git a/test/WorkerExtensionTests/Table/TableClientConverterTests.cs b/test/WorkerExtensionTests/Table/TableClientConverterTests.cs
new file mode 100644
index 000000000..c3f48c2fa
--- /dev/null
+++ b/test/WorkerExtensionTests/Table/TableClientConverterTests.cs
@@ -0,0 +1,262 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Azure;
+using Azure.Data.Tables;
+using Google.Protobuf;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Extensions.Tables.Config;
+using Microsoft.Azure.Functions.Worker.Extensions.Tables;
+using Microsoft.Azure.Functions.Worker.Extensions.Tables.TypeConverters;
+using Microsoft.Azure.Functions.Worker.Converters;
+using Microsoft.Azure.Functions.Worker.Grpc.Messages;
+using Microsoft.Azure.Functions.Worker.Tests.Converters;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Azure.Functions.WorkerExtension.Tests.Table
+{
+ public class TableClientConverterTests
+ {
+ private TableClientConverter _tableConverter;
+ private Mock _mockTableServiceClient;
+
+ public TableClientConverterTests()
+ {
+ var host = new HostBuilder().ConfigureFunctionsWorkerDefaults((WorkerOptions options) => { }).Build();
+ var logger = host.Services.GetService>();
+
+ _mockTableServiceClient = new Mock();
+
+ var mockTableOptions = new Mock();
+ mockTableOptions
+ .Setup(m => m.CreateClient())
+ .Returns(_mockTableServiceClient.Object);
+
+ var mockTablesOptionsSnapshot = new Mock>();
+ mockTablesOptionsSnapshot
+ .Setup(m => m.Get(It.IsAny()))
+ .Returns(mockTableOptions.Object);
+
+ _tableConverter = new TableClientConverter(mockTablesOptionsSnapshot.Object, logger);
+ }
+
+ [Fact]
+ public async Task ConvertAsync_SourceAsObject_ReturnsUnhandled()
+ {
+ var context = new TestConverterContext(typeof(string), new object());
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
+ }
+
+
+ [Fact]
+ public async Task ConvertAsync_SourceAsModelBindingData_ReturnsSuccess()
+ {
+ object source = GetTestGrpcModelBindingData(GetTableClientBinaryData());
+ var result = new Mock();
+ var context = new TestConverterContext(typeof(TableClient), source);
+
+ _mockTableServiceClient
+ .Setup(c => c.GetTableClient(Constants.TableName))
+ .Returns((TableClient)result.Object);
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Succeeded, conversionResult.Status);
+
+ }
+
+ [Fact]
+ public async Task ConvertAsync_SourceAsCollectionModelBindingData_ReturnsSuccess()
+ {
+ object source = GetTestGrpcModelBindingData(GetTableEntityBinaryData());
+ var context = new TestConverterContext(typeof(IEnumerable), source);
+ var mockResponse = new Mock();
+ var tableClient = new Mock();
+
+ tableClient
+ .Setup(c => c.GetEntityAsync(It.IsAny(), It.IsAny(), null, default))
+ .ReturnsAsync(Response.FromValue(new TableEntity(It.IsAny(), It.IsAny()), mockResponse.Object));
+
+ _mockTableServiceClient
+ .Setup(c => c.GetTableClient(Constants.TableName))
+ .Returns(tableClient.Object);
+
+ var expectedOutput = Page.FromValues(new List{ new TableEntity("partitionKey", "rowKey") }, continuationToken: null, mockResponse.Object);
+
+ tableClient
+ .Setup(c => c.QueryAsync(It.IsAny(), null, null, default))
+ .Returns(AsyncPageable.FromPages(new List> { expectedOutput }));
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
+ }
+
+
+ [Fact]
+ public async Task ConvertAsync_SourceAsCollectionModelBindingData_TableEntity_ReturnsSuccess()
+ {
+ object source = GetTestGrpcModelBindingData(GetTableEntityBinaryData());
+ var context = new TestConverterContext(typeof(TableEntity), source);
+ var mockResponse = new Mock();
+ var tableClient = new Mock();
+
+ tableClient
+ .Setup(c => c.GetEntityAsync(It.IsAny(), It.IsAny(), null, default))
+ .ReturnsAsync(Response.FromValue(new TableEntity(It.IsAny(), It.IsAny()), mockResponse.Object));
+
+ _mockTableServiceClient
+ .Setup(c => c.GetTableClient(Constants.TableName))
+ .Returns(tableClient.Object);
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
+ }
+
+ [Fact]
+ public async Task ConvertAsync_SourceAsModelBindingData_ReturnsFailed()
+ {
+ object source = GetTestGrpcModelBindingData(GetTableClientBinaryData());
+ var result = new Mock();
+ var context = new TestConverterContext(typeof(TableClient), source);
+
+ _mockTableServiceClient
+ .Setup(c => c.GetTableClient(Constants.TableName))
+ .Throws(new Exception());
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Failed, conversionResult.Status);
+ }
+
+ [Fact]
+ public async Task ConvertAsync_WrongModelBindingData_ReturnsFailed()
+ {
+ object source = GetTestGrpcModelBindingData(GetWrongBinaryData());
+ var result = new Mock();
+ var context = new TestConverterContext(typeof(TableClient), source);
+
+ _mockTableServiceClient
+ .Setup(c => c.GetTableClient(Constants.TableName))
+ .Returns(result.Object);
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Failed, conversionResult.Status);
+ }
+
+ [Fact]
+ public async Task ConvertAsync_ContentSource_AsObject_ReturnsUnhandled()
+ {
+ var context = new TestConverterContext(typeof(TableClient), new Object());
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
+ }
+
+ [Fact]
+ public async Task ConvertAsync_ModelBindingData_Null_ReturnsUnhandled()
+ {
+ var context = new TestConverterContext(typeof(TableClient), null);
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
+ }
+
+ [Fact] // Should we fail if the result is ever null?
+ public async Task ConvertAsync_ResultIsNull_ReturnsUnhandled()
+ {
+ var grpcModelBindingData = GetTestGrpcModelBindingData(GetTableClientBinaryData());
+ var context = new TestConverterContext(typeof(TableClient), grpcModelBindingData);
+
+ _mockTableServiceClient
+ .Setup(c => c.GetTableClient(Constants.TableName))
+ .Returns((TableClient)null);
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
+ }
+
+ [Fact]
+ public async Task ConvertAsync_ModelBindingDataSource_NotCosmosExtension_ReturnsUnhandled()
+ {
+ var grpcModelBindingData = GetTestGrpcModelBindingData(GetTableClientBinaryData(), source: "anotherExtensions");
+ var context = new TestConverterContext(typeof(TableClient), grpcModelBindingData);
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
+ }
+
+ [Fact]
+ public async Task ConvertAsync_ModelBindingDataContentType_Unsupported_ReturnsFailed()
+ {
+ var grpcModelBindingData = GetTestGrpcModelBindingData(GetTableClientBinaryData(), contentType: "binary");
+ var context = new TestConverterContext(typeof(TableClient), grpcModelBindingData);
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Failed, conversionResult.Status);
+ Assert.Equal("Unexpected content-type. Currently only 'application/json' is supported.", conversionResult.Error.Message);
+ }
+
+
+ private BinaryData GetWrongBinaryData()
+ {
+ return new BinaryData("{" + "\"Connection\" : \"Connection\"" + "}");
+ }
+
+ private BinaryData GetTableClientBinaryData()
+ {
+ return new BinaryData("{" +
+ "\"TableName\" : \"TableName\"" +
+ "}");
+ }
+
+ private BinaryData GetTableEntityBinaryData()
+ {
+ return new BinaryData("{" +
+ "\"Connection\" : \"Connection\"," +
+ "\"TableName\" : \"TableName\"," +
+ "\"PartitionKey\" : \"PartitionKey\"," +
+ "\"RowKey\" : \"RowKey\"" +
+ "}");
+ }
+
+ private BinaryData GetBadEntityBinaryData()
+ {
+ return new BinaryData("{" +
+ "\"Connection\" : \"Connection\"," +
+ "\"TableName\" : \"TableName\"," +
+ "\"PartitionKey\" : \"PartitionKey\"" +
+ "}");
+ }
+
+
+ private GrpcModelBindingData GetTestGrpcModelBindingData(BinaryData binaryData, string source = "AzureStorageTables", string contentType = "application/json")
+ {
+ return new GrpcModelBindingData(new ModelBindingData()
+ {
+ Version = "1.0",
+ Source = "AzureStorageTables",
+ Content = ByteString.CopyFrom(binaryData),
+ ContentType = contentType
+ });
+ }
+ }
+}
diff --git a/test/WorkerExtensionTests/Table/TableEntityConverterTests.cs b/test/WorkerExtensionTests/Table/TableEntityConverterTests.cs
new file mode 100644
index 000000000..91f20b32a
--- /dev/null
+++ b/test/WorkerExtensionTests/Table/TableEntityConverterTests.cs
@@ -0,0 +1,259 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Azure;
+using Azure.Data.Tables;
+using Google.Protobuf;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Extensions.Tables.Config;
+using Microsoft.Azure.Functions.Worker.Extensions.Tables;
+using Microsoft.Azure.Functions.Worker.Extensions.Tables.TypeConverters;
+using Microsoft.Azure.Functions.Worker.Converters;
+using Microsoft.Azure.Functions.Worker.Grpc.Messages;
+using Microsoft.Azure.Functions.Worker.Tests.Converters;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Azure.Functions.WorkerExtension.Tests.Table
+{
+ public class TableEntityConverterTests
+ {
+ private TableEntityConverter _tableConverter;
+ private Mock _mockTableServiceClient;
+
+ public TableEntityConverterTests()
+ {
+ var host = new HostBuilder().ConfigureFunctionsWorkerDefaults((WorkerOptions options) => { }).Build();
+ var logger = host.Services.GetService>();
+
+ _mockTableServiceClient = new Mock();
+
+ var mockTableOptions = new Mock();
+ mockTableOptions
+ .Setup(m => m.CreateClient())
+ .Returns(_mockTableServiceClient.Object);
+
+ var mockTablesOptionsSnapshot = new Mock>();
+ mockTablesOptionsSnapshot
+ .Setup(m => m.Get(It.IsAny()))
+ .Returns(mockTableOptions.Object);
+
+ _tableConverter = new TableEntityConverter(mockTablesOptionsSnapshot.Object, logger);
+ }
+
+ [Fact]
+ public async Task ConvertAsync_SourceAsObject_ReturnsUnhandled()
+ {
+ var context = new TestConverterContext(typeof(string), new object());
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
+ }
+
+
+ [Fact]
+ public async Task ConvertAsync_SourceAsModelBindingData_ReturnsUnhandled()
+ {
+ object source = GetTestGrpcModelBindingData(GetTableClientBinaryData());
+ var result = new Mock();
+ var context = new TestConverterContext(typeof(TableClient), source);
+
+ _mockTableServiceClient
+ .Setup(c => c.GetTableClient(Constants.TableName))
+ .Returns((TableClient)result.Object);
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
+
+ }
+
+ [Fact]
+ public async Task ConvertAsync_SourceAsCollectionModelBindingData_ReturnsSuccess()
+ {
+ object source = GetTestGrpcModelBindingData(GetTableEntityBinaryData());
+ var context = new TestConverterContext(typeof(IEnumerable), source);
+ var mockResponse = new Mock();
+ var tableClient = new Mock();
+
+ tableClient
+ .Setup(c => c.GetEntityAsync(It.IsAny(), It.IsAny(), null, default))
+ .ReturnsAsync(Response.FromValue(new TableEntity(It.IsAny(), It.IsAny()), mockResponse.Object));
+
+ _mockTableServiceClient
+ .Setup(c => c.GetTableClient(Constants.TableName))
+ .Returns(tableClient.Object);
+
+ var expectedOutput = Page.FromValues(new List { new TableEntity("partitionKey", "rowKey") }, continuationToken: null, mockResponse.Object);
+
+ tableClient
+ .Setup(c => c.QueryAsync(It.IsAny(), null, null, default))
+ .Returns(AsyncPageable.FromPages(new List> { expectedOutput }));
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
+ }
+
+
+ [Fact]
+ public async Task ConvertAsync_SourceAsCollectionModelBindingData_TableEntity_ReturnsSuccess()
+ {
+ object source = GetTestGrpcModelBindingData(GetTableEntityBinaryData());
+ var context = new TestConverterContext(typeof(TableEntity), source);
+ var mockResponse = new Mock();
+ var tableClient = new Mock();
+
+ tableClient
+ .Setup(c => c.GetEntityAsync(It.IsAny(), It.IsAny(), null, default))
+ .ReturnsAsync(Response.FromValue(new TableEntity(It.IsAny(), It.IsAny()), mockResponse.Object));
+
+ _mockTableServiceClient
+ .Setup(c => c.GetTableClient(Constants.TableName))
+ .Returns(tableClient.Object);
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Succeeded, conversionResult.Status);
+ Assert.Equal(typeof(TableEntity), conversionResult.Value.GetType());
+ }
+
+ [Fact]
+ public async Task ConvertAsync_SourceAsCollectionModelBindingData_BadTableEntity_ReturnsSuccess()
+ {
+ object source = GetTestGrpcModelBindingData(GetBadEntityBinaryData());
+ var context = new TestConverterContext(typeof(TableEntity), source);
+ var mockResponse = new Mock();
+ var tableClient = new Mock();
+
+ tableClient
+ .Setup(c => c.GetEntityAsync(It.IsAny(), It.IsAny(), null, default))
+ .ReturnsAsync(Response.FromValue(new TableEntity(It.IsAny(), It.IsAny()), mockResponse.Object));
+
+ _mockTableServiceClient
+ .Setup(c => c.GetTableClient(Constants.TableName))
+ .Returns(tableClient.Object);
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Failed, conversionResult.Status);
+ }
+
+ [Fact]
+ public async Task ConvertAsync_ContentSource_AsObject_ReturnsUnhandled()
+ {
+ var context = new TestConverterContext(typeof(TableEntity), new Object());
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
+ }
+
+ [Fact]
+ public async Task ConvertAsync_ModelBindingData_Null_ReturnsUnhandled()
+ {
+ var context = new TestConverterContext(typeof(TableEntity), null);
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
+ }
+
+ [Fact] // Should we fail if the result is ever null?
+ public async Task ConvertAsync_ResultIsNull_ReturnsUnhandled()
+ {
+ var grpcModelBindingData = GetTestGrpcModelBindingData(GetTableEntityBinaryData());
+ var context = new TestConverterContext(typeof(TableEntity), grpcModelBindingData);
+
+ var tableClient = new Mock();
+ var mockResponse = new Mock();
+
+ tableClient
+ .Setup(c => c.GetEntityAsync(It.IsAny(), It.IsAny(), null, default))
+ .ReturnsAsync(Response.FromValue((TableEntity)null, mockResponse.Object));
+
+ _mockTableServiceClient
+ .Setup(c => c.GetTableClient(Constants.TableName))
+ .Returns(tableClient.Object);
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
+ }
+
+ [Fact]
+ public async Task ConvertAsync_ModelBindingDataSource_NotCosmosExtension_ReturnsUnhandled()
+ {
+ var grpcModelBindingData = GetTestGrpcModelBindingData(GetTableEntityBinaryData(), source: "anotherExtensions");
+ var context = new TestConverterContext(typeof(TableEntity), grpcModelBindingData);
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
+ }
+
+ [Fact]
+ public async Task ConvertAsync_ModelBindingDataContentType_Unsupported_ReturnsFailed()
+ {
+ var grpcModelBindingData = GetTestGrpcModelBindingData(GetTableEntityBinaryData(), contentType: "binary");
+ var context = new TestConverterContext(typeof(TableEntity), grpcModelBindingData);
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Failed, conversionResult.Status);
+ Assert.Equal("Unexpected content-type. Currently only 'application/json' is supported.", conversionResult.Error.Message);
+ }
+
+
+ private BinaryData GetWrongBinaryData()
+ {
+ return new BinaryData("{" + "\"Connection\" : \"Connection\"" + "}");
+ }
+
+ private BinaryData GetTableClientBinaryData()
+ {
+ return new BinaryData("{" +
+ "\"TableName\" : \"TableName\"" +
+ "}");
+ }
+
+ private BinaryData GetTableEntityBinaryData()
+ {
+ return new BinaryData("{" +
+ "\"Connection\" : \"Connection\"," +
+ "\"TableName\" : \"TableName\"," +
+ "\"PartitionKey\" : \"PartitionKey\"," +
+ "\"RowKey\" : \"RowKey\"" +
+ "}");
+ }
+
+ private BinaryData GetBadEntityBinaryData()
+ {
+ return new BinaryData("{" +
+ "\"Connection\" : \"Connection\"," +
+ "\"TableName\" : \"TableName\"," +
+ "\"PartitionKey\" : \"PartitionKey\"" +
+ "}");
+ }
+
+
+ private GrpcModelBindingData GetTestGrpcModelBindingData(BinaryData binaryData, string source = "AzureStorageTables", string contentType = "application/json")
+ {
+ return new GrpcModelBindingData(new ModelBindingData()
+ {
+ Version = "1.0",
+ Source = source,
+ Content = ByteString.CopyFrom(binaryData),
+ ContentType = contentType
+ });
+ }
+ }
+}
diff --git a/test/WorkerExtensionTests/Table/TableEntityEnumerableConverterTests.cs b/test/WorkerExtensionTests/Table/TableEntityEnumerableConverterTests.cs
new file mode 100644
index 000000000..d179df8c7
--- /dev/null
+++ b/test/WorkerExtensionTests/Table/TableEntityEnumerableConverterTests.cs
@@ -0,0 +1,230 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Azure;
+using Azure.Data.Tables;
+using Google.Protobuf;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Extensions.Tables.Config;
+using Microsoft.Azure.Functions.Worker.Extensions.Tables;
+using Microsoft.Azure.Functions.Worker.Extensions.Tables.TypeConverters;
+using Microsoft.Azure.Functions.Worker.Converters;
+using Microsoft.Azure.Functions.Worker.Grpc.Messages;
+using Microsoft.Azure.Functions.Worker.Tests.Converters;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Azure.Functions.WorkerExtension.Tests.Table
+{
+ public class TableEntityEnumerableConverterTests
+ {
+ private TableEntityEnumerableConverter _tableConverter;
+ private Mock _mockTableServiceClient;
+
+ public TableEntityEnumerableConverterTests()
+ {
+ var host = new HostBuilder().ConfigureFunctionsWorkerDefaults((WorkerOptions options) => { }).Build();
+ var logger = host.Services.GetService>();
+
+ _mockTableServiceClient = new Mock();
+
+ var mockTableOptions = new Mock();
+ mockTableOptions
+ .Setup(m => m.CreateClient())
+ .Returns(_mockTableServiceClient.Object);
+
+ var mockTablesOptionsSnapshot = new Mock>();
+ mockTablesOptionsSnapshot
+ .Setup(m => m.Get(It.IsAny()))
+ .Returns(mockTableOptions.Object);
+
+ _tableConverter = new TableEntityEnumerableConverter(mockTablesOptionsSnapshot.Object, logger);
+ }
+
+ [Fact]
+ public async Task ConvertAsync_SourceAsObject_ReturnsUnhandled()
+ {
+ var context = new TestConverterContext(typeof(string), new object());
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
+ }
+
+
+ [Fact]
+ public async Task ConvertAsync_SourceAsModelBindingData_ReturnsUnhandled()
+ {
+ object source = GetTestGrpcModelBindingData(GetTableClientBinaryData());
+ var result = new Mock();
+ var context = new TestConverterContext(typeof(TableClient), source);
+
+ _mockTableServiceClient
+ .Setup(c => c.GetTableClient(Constants.TableName))
+ .Returns((TableClient)result.Object);
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
+
+ }
+
+ [Fact]
+ public async Task ConvertAsync_SourceAsCollectionModelBindingData_ReturnsSuccess()
+ {
+ object source = GetTestGrpcModelBindingData(GetTableEntityBinaryData());
+ var context = new TestConverterContext(typeof(IEnumerable), source);
+ var mockResponse = new Mock();
+ var tableClient = new Mock();
+
+ tableClient
+ .Setup(c => c.GetEntityAsync(It.IsAny(), It.IsAny(), null, default))
+ .ReturnsAsync(Response.FromValue(new TableEntity(It.IsAny(), It.IsAny()), mockResponse.Object));
+
+ _mockTableServiceClient
+ .Setup(c => c.GetTableClient(Constants.TableName))
+ .Returns(tableClient.Object);
+
+ var expectedOutput = Page.FromValues(new List { new TableEntity("partitionKey", "rowKey") }, continuationToken: null, mockResponse.Object);
+
+ tableClient
+ .Setup(c => c.QueryAsync(It.IsAny(), null, null, default))
+ .Returns(AsyncPageable.FromPages(new List> { expectedOutput }));
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Succeeded, conversionResult.Status);
+ }
+
+
+ [Fact]
+ public async Task ConvertAsync_SourceAsCollectionModelBindingData_TableEntity_ReturnsSuccess()
+ {
+ object source = GetTestGrpcModelBindingData(GetTableEntityBinaryData());
+ var context = new TestConverterContext(typeof(TableEntity), source);
+ var mockResponse = new Mock();
+ var tableClient = new Mock();
+
+ tableClient
+ .Setup(c => c.GetEntityAsync(It.IsAny(), It.IsAny(), null, default))
+ .ReturnsAsync(Response.FromValue(new TableEntity(It.IsAny(), It.IsAny()), mockResponse.Object));
+
+ _mockTableServiceClient
+ .Setup(c => c.GetTableClient(Constants.TableName))
+ .Returns(tableClient.Object);
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
+ }
+
+ [Fact]
+ public async Task ConvertAsync_SourceAsCollectionModelBindingData_BadTableEntity_ReturnsSuccess()
+ {
+ object source = GetTestGrpcModelBindingData(GetBadEntityBinaryData());
+ var context = new TestConverterContext(typeof(TableEntity), source);
+ var mockResponse = new Mock();
+ var tableClient = new Mock();
+
+ tableClient
+ .Setup(c => c.GetEntityAsync(It.IsAny(), It.IsAny(), null, default))
+ .ReturnsAsync(Response.FromValue(new TableEntity(It.IsAny(), It.IsAny()), mockResponse.Object));
+
+ _mockTableServiceClient
+ .Setup(c => c.GetTableClient(Constants.TableName))
+ .Returns(tableClient.Object);
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
+ }
+
+ [Fact]
+ public async Task ConvertAsync_ContentSource_AsObject_ReturnsUnhandled()
+ {
+ var context = new TestConverterContext(typeof(IEnumerable), new Object());
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
+ }
+
+ [Fact]
+ public async Task ConvertAsync_ModelBindingData_Null_ReturnsUnhandled()
+ {
+ var context = new TestConverterContext(typeof(IEnumerable), null);
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
+ }
+
+ [Fact]
+ public async Task ConvertAsync_ModelBindingDataSource_NotCosmosExtension_ReturnsUnhandled()
+ {
+ var grpcModelBindingData = GetTestGrpcModelBindingData(GetTableEntityBinaryData(), source: "anotherExtensions");
+ var context = new TestConverterContext(typeof(IEnumerable), grpcModelBindingData);
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
+ }
+
+ [Fact]
+ public async Task ConvertAsync_ModelBindingDataContentType_Unsupported_ReturnsFailed()
+ {
+ var grpcModelBindingData = GetTestGrpcModelBindingData(GetTableEntityBinaryData(), contentType: "binary");
+ var context = new TestConverterContext(typeof(IEnumerable), grpcModelBindingData);
+
+ var conversionResult = await _tableConverter.ConvertAsync(context);
+
+ Assert.Equal(ConversionStatus.Failed, conversionResult.Status);
+ Assert.Equal("Unexpected content-type. Currently only 'application/json' is supported.", conversionResult.Error.Message);
+ }
+
+ private BinaryData GetTableClientBinaryData()
+ {
+ return new BinaryData("{" +
+ "\"TableName\" : \"TableName\"" +
+ "}");
+ }
+
+ private BinaryData GetTableEntityBinaryData()
+ {
+ return new BinaryData("{" +
+ "\"Connection\" : \"Connection\"," +
+ "\"TableName\" : \"TableName\"," +
+ "\"PartitionKey\" : \"PartitionKey\"," +
+ "\"RowKey\" : \"RowKey\"" +
+ "}");
+ }
+
+ private BinaryData GetBadEntityBinaryData()
+ {
+ return new BinaryData("{" +
+ "\"Connection\" : \"Connection\"," +
+ "\"TableName\" : \"TableName\"," +
+ "\"PartitionKey\" : \"PartitionKey\"" +
+ "}");
+ }
+
+
+ private GrpcModelBindingData GetTestGrpcModelBindingData(BinaryData binaryData, string source = "AzureStorageTables", string contentType = "application/json")
+ {
+ return new GrpcModelBindingData(new ModelBindingData()
+ {
+ Version = "1.0",
+ Source = source,
+ Content = ByteString.CopyFrom(binaryData),
+ ContentType = contentType
+ });
+ }
+ }
+}
diff --git a/test/WorkerExtensionTests/Table/TablesBindingOptionsSetupTest.cs b/test/WorkerExtensionTests/Table/TablesBindingOptionsSetupTest.cs
new file mode 100644
index 000000000..c93275fb1
--- /dev/null
+++ b/test/WorkerExtensionTests/Table/TablesBindingOptionsSetupTest.cs
@@ -0,0 +1,93 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Azure.Core;
+using Azure.Data.Tables;
+using Microsoft.Azure.Functions.Worker.Extensions.Tables.Config;
+using Microsoft.Extensions.Azure;
+using Microsoft.Extensions.Configuration;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Azure.Functions.WorkerExtension.Tests.Table
+{
+ public class TablesBindingOptionsSetupTest
+ {
+ private Mock configuration;
+ private Mock componentFactory;
+ public TablesBindingOptionsSetupTest()
+ {
+ configuration = new Mock();
+ componentFactory = new Mock();
+ }
+
+ [Fact]
+ public void Configure_With_Default_Name()
+ {
+ var configSection = new Mock();
+
+ configSection.Setup(c => c.Key).Returns("key");
+ configSection.Setup(c => c.Value).Returns("connectionString");
+ configuration.Setup(c => c.GetSection("AzureWebJobsStorage")).Returns(configSection.Object);
+
+ var tableBindingOptions = new Mock();
+ var tokenCredential = new Mock();
+
+ componentFactory.Setup(c => c.CreateClientOptions(typeof(TableClientOptions), null, configSection.Object)).Returns(null);
+ componentFactory.Setup(c => c.CreateTokenCredential(configSection.Object)).Returns(tokenCredential.Object);
+
+ var tablesBindingOptionsSetup = new Mock(configuration.Object, componentFactory.Object);
+ tablesBindingOptionsSetup.Object.Configure(tableBindingOptions.Object);
+
+ Assert.Equal("connectionString", tableBindingOptions.Object.ConnectionString);
+ Assert.NotNull(tableBindingOptions.Object.Credential);
+ }
+
+ [Fact]
+ public void Configure_With_Given_Name()
+ {
+ var configSection = new Mock();
+ configSection.Setup(c => c.Key).Returns("key");
+ configSection.Setup(c => c.Value).Returns("connectionString");
+ configuration.Setup(c => c.GetSection("AzureWebJobsCustom")).Returns(configSection.Object);
+
+ var tableBindingOptions = new Mock();
+ var tokenCredential = new Mock();
+
+ componentFactory.Setup(c => c.CreateClientOptions(typeof(TableClientOptions), null, configSection.Object)).Returns(null);
+ componentFactory.Setup(c => c.CreateTokenCredential(configSection.Object)).Returns(tokenCredential.Object);
+
+ var tablesBindingOptionsSetup = new Mock(configuration.Object, componentFactory.Object);
+ tablesBindingOptionsSetup.Object.Configure("Custom",tableBindingOptions.Object);
+
+ Assert.Equal("connectionString", tableBindingOptions.Object.ConnectionString);
+ Assert.NotNull(tableBindingOptions.Object.Credential);
+ }
+
+ [Fact]
+ public void Configure_With_ServiceUri()
+ {
+ var configSection = new Mock();
+ configSection.Setup(c => c.Key).Returns("key");
+ configSection.Setup(c => c.Value).Returns("");
+
+ var inMemorySettings = new Dictionary {
+ {"AzureWebJobsStorage:accountName", "test"},
+ };
+
+ IConfiguration configurationReal = new ConfigurationBuilder()
+ .AddInMemoryCollection(inMemorySettings)
+ .Build();
+
+ var tableBindingOptions = new Mock();
+ var tokenCredential = new Mock();
+ componentFactory.Setup(c => c.CreateClientOptions(typeof(TableClientOptions), null, configSection.Object)).Returns(null);
+ componentFactory.Setup(c => c.CreateTokenCredential(configSection.Object)).Returns(tokenCredential.Object);
+
+ var tablesBindingOptionsSetup = new Mock(configurationReal, componentFactory.Object);
+ tablesBindingOptionsSetup.Object.Configure(tableBindingOptions.Object);
+
+ Assert.Null(tableBindingOptions.Object.ConnectionString);
+ Assert.Equal("https://test.table.core.windows.net/", tableBindingOptions.Object.ServiceUri.ToString());
+ }
+ }
+}
diff --git a/test/WorkerExtensionTests/WorkerExtensionTests.csproj b/test/WorkerExtensionTests/WorkerExtensionTests.csproj
index f394c115b..25339322c 100644
--- a/test/WorkerExtensionTests/WorkerExtensionTests.csproj
+++ b/test/WorkerExtensionTests/WorkerExtensionTests.csproj
@@ -25,6 +25,7 @@
+