diff --git a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/CHANGELOG.md b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/CHANGELOG.md index 3b2b47a9514eb..2f49052226ed3 100644 --- a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/CHANGELOG.md +++ b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/CHANGELOG.md @@ -12,6 +12,7 @@ ## 5.1.2 (2023-04-27) - Fixed bug where the blob container would scan from the beginning due not correctly updating the latest scan time. (#35145) +- Loosen parameter binding data parsing and validation to allow binding BlobContainerClient without blob name. (#37124) ## 5.1.1 (2023-03-24) - Bumped Azure.Core dependency from 1.28 and 1.30, fixing issue with headers being non-resilient to double dispose of the request. diff --git a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/BlobPath.cs b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/BlobPath.cs index 2902c33c2b8a2..0ca7d810a5dce 100644 --- a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/BlobPath.cs +++ b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/BlobPath.cs @@ -43,12 +43,12 @@ public override string ToString() return result; } - public static BlobPath ParseAndValidate(string value, bool isContainerBinding = false) + public static BlobPath ParseAndValidate(string value, bool isContainerBinding = false, bool isParameterBindingData = false) { string errorMessage; BlobPath path; - if (!TryParseAndValidate(value, out errorMessage, out path, isContainerBinding)) + if (!TryParseAndValidate(value, out errorMessage, out path, isContainerBinding, isParameterBindingData)) { throw new FormatException(errorMessage); } @@ -82,7 +82,7 @@ public static bool TryParseAbsUrl(string blobUrl, out BlobPath path) return false; } - public static bool TryParse(string value, bool isContainerBinding, out BlobPath path) + public static bool TryParse(string value, bool isContainerBinding, bool isParameterBindingData, out BlobPath path) { path = null; @@ -92,7 +92,7 @@ public static bool TryParse(string value, bool isContainerBinding, out BlobPath } int slashIndex = value.IndexOf('/'); - if (!isContainerBinding && slashIndex <= 0) + if (!isParameterBindingData && !isContainerBinding && slashIndex <= 0) { return false; } @@ -111,7 +111,7 @@ public static bool TryParse(string value, bool isContainerBinding, out BlobPath return true; } - private static bool TryParseAndValidate(string value, out string errorMessage, out BlobPath path, bool isContainerBinding = false) + private static bool TryParseAndValidate(string value, out string errorMessage, out BlobPath path, bool isContainerBinding = false, bool isParameterBindingData = false) { BlobPath possiblePath; @@ -122,7 +122,7 @@ private static bool TryParseAndValidate(string value, out string errorMessage, o return true; } - if (!TryParse(value, isContainerBinding, out possiblePath)) + if (!TryParse(value, isContainerBinding, isParameterBindingData, out possiblePath)) { errorMessage = $"Invalid blob path specified : '{value}'. Blob identifiers must be in the format 'container/blob'."; path = null; @@ -136,9 +136,9 @@ private static bool TryParseAndValidate(string value, out string errorMessage, o return false; } - // for container bindings, we allow an empty blob name/path + // for container bindings or parameter binding data, we allow an empty blob name/path string possibleErrorMessage; - if (!(isContainerBinding && string.IsNullOrEmpty(possiblePath.BlobName)) && + if (!((isContainerBinding || isParameterBindingData) && string.IsNullOrEmpty(possiblePath.BlobName)) && !BlobClientExtensions.IsValidBlobName(possiblePath.BlobName, out possibleErrorMessage)) { errorMessage = possibleErrorMessage; diff --git a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Config/BlobsExtensionConfigProvider.cs b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Config/BlobsExtensionConfigProvider.cs index 7191186a23e4c..87f5af28ce8e0 100644 --- a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Config/BlobsExtensionConfigProvider.cs +++ b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Config/BlobsExtensionConfigProvider.cs @@ -220,7 +220,7 @@ private async Task CreateBlobReference(BlobAttribute blobAttribute, Cancel private ParameterBindingData ConvertToParameterBindingData(BlobAttribute blobAttribute) { - var blobPath = BlobPath.ParseAndValidate(blobAttribute.BlobPath); + var blobPath = BlobPath.ParseAndValidate(blobAttribute.BlobPath, isParameterBindingData: true); return CreateParameterBindingData(blobAttribute.Connection, blobPath.BlobName, blobPath.ContainerName); } diff --git a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/StorageAnalyticsLogEntry.cs b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/StorageAnalyticsLogEntry.cs index 944e83ab65fc8..6bfde68671f02 100644 --- a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/StorageAnalyticsLogEntry.cs +++ b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/StorageAnalyticsLogEntry.cs @@ -147,7 +147,7 @@ public BlobPath ToBlobPath() } BlobPath blobPath; - if (!BlobPath.TryParse(path, false, out blobPath)) + if (!BlobPath.TryParse(path, false, false, out blobPath)) { return null; } diff --git a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/tests/BlobPathSourceTests.cs b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/tests/BlobPathSourceTests.cs index 978448b124c85..7eebadd6b1c88 100644 --- a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/tests/BlobPathSourceTests.cs +++ b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/tests/BlobPathSourceTests.cs @@ -25,7 +25,7 @@ private static IDictionary Match(string a, string b) { var pathA = BlobPathSource.Create(a); BlobPath pathB = null; - BlobPath.TryParse(b, false, out pathB); + BlobPath.TryParse(b, false, false, out pathB); IReadOnlyDictionary bindingData = pathA.CreateBindingData(pathB); diff --git a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/tests/BlobTests.cs b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/tests/BlobTests.cs index d294a27bdcddf..aaa9961de0eb2 100644 --- a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/tests/BlobTests.cs +++ b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/tests/BlobTests.cs @@ -8,6 +8,7 @@ using Azure.Storage.Blobs; using Azure.Storage.Blobs.Specialized; using Azure.Storage.Queues; +using BenchmarkDotNet.Engines; using Microsoft.Azure.WebJobs.Extensions.Storage.Common.Tests; using Microsoft.Extensions.Azure; using Microsoft.Extensions.Configuration; @@ -41,7 +42,7 @@ public async Task Blob_IfBoundToCloudBlockBlob_BindsAndCreatesContainerButNotBlo { // Act var prog = new BindToCloudBlockBlobProgram(); - IHost host = new HostBuilder() + var host = new HostBuilder() .ConfigureDefaultTestHost(prog, builder => { builder.AddAzureStorageBlobs() @@ -70,7 +71,7 @@ public async Task Blob_IfBoundToBlobClient_BindsAndCreatesContainerButNotBlob() { // Act var prog = new BindToBlobClientProgram(); - IHost host = new HostBuilder() + var host = new HostBuilder() .ConfigureDefaultTestHost(prog, builder => { builder.AddAzureStorageBlobs() @@ -126,7 +127,7 @@ public async Task Blob_IfBoundToParameterBindingData_CreatesParameterBindingData }).Build(); var program = new BindToParameterBindingData(); - IHost host = new HostBuilder() + var host = new HostBuilder() .ConfigureDefaultTestHost(program, builder => { builder.AddAzureStorageBlobs() @@ -154,6 +155,46 @@ public async Task Blob_IfBoundToParameterBindingData_CreatesParameterBindingData Assert.AreEqual(BlobName, resultBlobName); } + [Test] + public async Task Blob_IfBoundToParameterBindingData_Container_CreatesParameterBindingData() + { + // Arrange + string connectionString = AzuriteNUnitFixture.Instance.GetAzureAccount().ConnectionString; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary() + { + { "ConnectionStrings:AzureWebJobsStorage", connectionString } + }).Build(); + + var program = new BindToParameterBindingDataBlobContainer(); + var host = new HostBuilder() + .ConfigureDefaultTestHost(program, builder => + { + builder.AddAzureStorageBlobs() + .UseStorageServicesWithConfiguration(blobServiceClient, queueServiceClient, configuration); + }) + .Build(); + + var jobHost = host.GetJobHost(); + + // Act + await jobHost.CallAsync(nameof(BindToParameterBindingDataBlobContainer.Run)); + ParameterBindingData result = program.Result; + + Assert.NotNull(result); + + var blobData = result?.Content.ToObjectFromJson>(); + + // Assert + Assert.True(blobData.TryGetValue("Connection", out var resultConnection)); + Assert.True(blobData.TryGetValue("ContainerName", out var resultContainerName)); + Assert.True(blobData.TryGetValue("BlobName", out var resultBlobName)); + + Assert.AreEqual(ConnectionName, resultConnection); + Assert.AreEqual(ContainerName, resultContainerName); + Assert.IsEmpty(resultBlobName); + } + [Test] public async Task Blob_IfBoundToParameterBindingDataEnumerable_CreatesParameterBindingDataArray() { @@ -166,7 +207,7 @@ public async Task Blob_IfBoundToParameterBindingDataEnumerable_CreatesParameterB // Arrange var program = new BindToParameterBindingDataEnumerable(); - IHost host = new HostBuilder() + var host = new HostBuilder() .ConfigureDefaultTestHost(program, builder => { builder.AddAzureStorageBlobs() @@ -213,7 +254,7 @@ public async Task Blob_IfBoundToStringArray_CreatesStringArray() // Arrange var program = new BindToStringArray(); - IHost host = new HostBuilder() + var host = new HostBuilder() .ConfigureDefaultTestHost(program, builder => { builder.AddAzureStorageBlobs() @@ -252,7 +293,7 @@ public async Task Blob_IfBoundToParameterBindingDataArray_CreatesParameterBindin // Arrange var program = new BindToParameterBindingDataArray(); - IHost host = new HostBuilder() + var host = new HostBuilder() .ConfigureDefaultTestHost(program, builder => { builder.AddAzureStorageBlobs() @@ -364,6 +405,17 @@ public void Run( } } + private class BindToParameterBindingDataBlobContainer + { + public ParameterBindingData Result { get; set; } + + public void Run( + [Blob(ContainerName)] ParameterBindingData blobData) + { + this.Result = blobData; + } + } + private class BindToParameterBindingDataEnumerable { public IEnumerable Result { get; set; }