Skip to content

Commit

Permalink
[Storage][Webjobs] Bind to BlobClient (Azure#16336)
Browse files Browse the repository at this point in the history
* can bind [Blob] to BlobClient.

* this works.

* update snippet to use blobclient.

* remove todo.

* add doc strings.

* scope new converter to blobtrigger.

* remove unused part.
  • Loading branch information
kasobol-msft authored and annelo-msft committed Feb 17, 2021
1 parent b3811b8 commit 2a5c344
Show file tree
Hide file tree
Showing 10 changed files with 189 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,13 @@ public static class BlobFunction_WriteStream

### Binding to Azure Storage Blob SDK types

```C# Snippet:BlobFunction_BlobBaseClient
public static class BlobFunction_BlobBaseClient
```C# Snippet:BlobFunction_BlobClient
public static class BlobFunction_BlobClient
{
[FunctionName("BlobFunction")]
public static async Task Run(
[BlobTrigger("sample-container/sample-blob-1")] BlobBaseClient blobTriggerClient,
[Blob("sample-container/sample-blob-2")] BlobBaseClient blobClient,
[BlobTrigger("sample-container/sample-blob-1")] BlobClient blobTriggerClient,
[Blob("sample-container/sample-blob-2")] BlobClient blobClient,
ILogger logger)
{
BlobProperties blobTriggerProperties = await blobTriggerClient.GetPropertiesAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Azure.Storage.Blobs.Specialized;
using Azure.WebJobs.Extensions.Storage.Common.Tests;
Expand All @@ -22,7 +23,7 @@ public class BlobExtensionSamples
[TestCase(typeof(BlobFunction_String))]
[TestCase(typeof(BlobFunction_ReadStream))]
[TestCase(typeof(BlobFunction_WriteStream))]
[TestCase(typeof(BlobFunction_BlobBaseClient))]
[TestCase(typeof(BlobFunction_BlobClient))]
public async Task Run_BlobFunction(Type programType)
{
var containerClient = AzuriteNUnitFixture.Instance.GetBlobServiceClient().GetBlobContainerClient("sample-container");
Expand Down Expand Up @@ -96,13 +97,13 @@ public static async Task Run(
}
#endregion

#region Snippet:BlobFunction_BlobBaseClient
public static class BlobFunction_BlobBaseClient
#region Snippet:BlobFunction_BlobClient
public static class BlobFunction_BlobClient
{
[FunctionName("BlobFunction")]
public static async Task Run(
[BlobTrigger("sample-container/sample-blob-1")] BlobBaseClient blobTriggerClient,
[Blob("sample-container/sample-blob-2")] BlobBaseClient blobClient,
[BlobTrigger("sample-container/sample-blob-1")] BlobClient blobTriggerClient,
[Blob("sample-container/sample-blob-2")] BlobClient blobClient,
ILogger logger)
{
BlobProperties blobTriggerProperties = await blobTriggerClient.GetPropertiesAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ internal static class StorageBlobContainerExtensions
public static Task<BlobBaseClient> GetBlobReferenceForArgumentTypeAsync(this BlobContainerClient container,
string blobName, Type argumentType, CancellationToken cancellationToken)
{
if (argumentType == typeof(BlockBlobClient))
if (argumentType == typeof(BlobClient))
{
BlobBaseClient blob = container.GetBlobClient(blobName);
return Task.FromResult(blob);
}
else if (argumentType == typeof(BlockBlobClient))
{
BlobBaseClient blob = container.GetBlockBlobClient(blobName);
return Task.FromResult(blob);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Specialized;
using Microsoft.Azure.WebJobs.Description;

Expand Down Expand Up @@ -40,6 +41,7 @@ namespace Microsoft.Azure.WebJobs
/// The parameter type can be CloudBlobContainer, CloudBlobDirectory or <see cref="IEnumerable{T}"/>
/// of one of the following element types:
/// <list type = "bullet" >
/// <item><description><see cref="BlobClient"/></description></item>
/// <item><description><see cref="BlobBaseClient"/></description></item>
/// <item><description><see cref="AppendBlobClient"/></description></item>
/// <item><description><see cref="BlockBlobClient"/></description></item>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Diagnostics;
using System.IO;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Specialized;
using Microsoft.Azure.WebJobs.Description;

Expand All @@ -18,6 +19,7 @@ namespace Microsoft.Azure.WebJobs
/// <remarks>
/// The method parameter type can be one of the following:
/// <list type="bullet">
/// <item><description><see cref="BlobClient"/></description></item>
/// <item><description><see cref="BlobBaseClient"/></description></item>
/// <item><description><see cref="AppendBlobClient"/></description></item>
/// <item><description><see cref="BlockBlobClient"/></description></item>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,17 @@ private void InitilizeBlobBindings(ExtensionConfigContext context)
// Normal blob
// These are not converters because Blob/Page/Append affects how we *create* the blob.
rule.BindToInput<BlockBlobClient>((attr, cts) => CreateBlobReference<BlockBlobClient>(attr, cts));

rule.BindToInput<PageBlobClient>((attr, cts) => CreateBlobReference<PageBlobClient>(attr, cts));

rule.BindToInput<AppendBlobClient>((attr, cts) => CreateBlobReference<AppendBlobClient>(attr, cts));

// TODO (kasobol-msft) figure out how to add binding to BlobClient.
rule.BindToInput<BlobClient>((attr, cts) => CreateBlobReference<BlobClient>(attr, cts));
rule.BindToInput<BlobBaseClient>((attr, cts) => CreateBlobReference<BlobBaseClient>(attr, cts));

// CloudBlobStream's derived functionality is only relevant to writing. check derived functionality
rule.When("Access", FileAccess.Write).
BindToInput<Stream>(ConvertToCloudBlobStreamAsync);

RegisterCommonConverters(rule);
rule.AddConverter(new StorageBlobConverter<BlobClient>());
}

private void InitializeBlobTriggerBindings(ExtensionConfigContext context)
Expand All @@ -88,17 +88,20 @@ private void InitializeBlobTriggerBindings(ExtensionConfigContext context)
rule.AddConverter<BlobBaseClient, DirectInvokeString>(blob => new DirectInvokeString(blob.GetBlobPath()));
rule.AddConverter<DirectInvokeString, BlobBaseClient>(ConvertFromInvokeString);

// Common converters shared between [Blob] and [BlobTrigger]
RegisterCommonConverters(rule);
rule.AddConverter<BlobBaseClient, BlobClient>(ConvertBlobBaseClientToBlobClient);
}

// Trigger already has the IStorageBlob. Whereas BindToInput defines: Attr-->Stream.
#pragma warning disable CS0618 // Type or member is obsolete. FluentBindingRule is "Not ready for public consumption."
private void RegisterCommonConverters<T>(FluentBindingRule<T> rule) where T : Attribute
#pragma warning restore CS0618 // Type or member is obsolete
{
// Converter manager already has Stream-->Byte[],String,TextReader
context.AddConverter<BlobBaseClient, Stream>(ConvertToStreamAsync);

rule.AddConverter<BlobBaseClient, Stream>(ConvertToStreamAsync);
// Blob type is a property of an existing blob.
// $$$ did we lose CloudBlob. That's a base class for Cloud*Blob, but does not implement ICloudBlob?
context.AddConverter(new StorageBlobConverter<AppendBlobClient>());
context.AddConverter(new StorageBlobConverter<BlockBlobClient>());
context.AddConverter(new StorageBlobConverter<PageBlobClient>());
rule.AddConverter(new StorageBlobConverter<AppendBlobClient>());
rule.AddConverter(new StorageBlobConverter<BlockBlobClient>());
rule.AddConverter(new StorageBlobConverter<PageBlobClient>());
}

#region Container rules
Expand Down Expand Up @@ -134,8 +137,8 @@ private class BlobCollectionType : OpenType
{
private static readonly Type[] _types = new Type[]
{
// TODO (kasobol-msft) figure out how to introduce BlobClient binding
typeof(BlobBaseClient),
typeof(BlobClient),
typeof(BlockBlobClient),
typeof(PageBlobClient),
typeof(AppendBlobClient),
Expand Down Expand Up @@ -191,7 +194,15 @@ private async Task<IEnumerable<T>> ConvertBlobs(IAsyncEnumerable<BlobItem> blobI
switch (blobItem.Properties.BlobType)
{
case BlobType.Block:
src = blobContainerClient.GetBlockBlobClient(blobItem.Name);
if (typeof(T) == typeof(BlobClient))
{
// BlobClient is simplified version of BlockBlobClient, i.e. upload results in creation of block blob.
src = blobContainerClient.GetBlobClient(blobItem.Name);
}
else
{
src = blobContainerClient.GetBlockBlobClient(blobItem.Name);
}
break;
case BlobType.Append:
src = blobContainerClient.GetAppendBlobClient(blobItem.Name);
Expand Down Expand Up @@ -254,6 +265,22 @@ private async Task<BlobBaseClient> ConvertFromInvokeString(DirectInvokeString in
return blob.Item1;
}

// BlobClient is simplified version of BlockBlobClient, i.e. upload results in creation of block blob.
private BlobClient ConvertBlobBaseClientToBlobClient(BlobBaseClient input, BlobTriggerAttribute attr)
{
if (input is BlockBlobClient)
{
var client = _blobServiceClientProvider.Get(attr.Connection);
var blob = client.GetBlobContainerClient(input.BlobContainerName).GetBlobClient(input.Name);

return blob;
}
else
{
throw new InvalidOperationException($"The blob is not an {typeof(BlockBlobClient).Name}.");
}
}

private async Task<Stream> CreateStreamAsync(
BlobAttribute blobAttribute,
ValueBindingContext context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,35 @@ public async Task Blob_IfBoundToCloudBlockBlob_BindsAndCreatesContainerButNotBlo
Assert.False(await blob.ExistsAsync());
}

[Test]
public async Task Blob_IfBoundToBlobClient_BindsAndCreatesContainerButNotBlob()
{
// Act
var prog = new BindToBlobClientProgram();
IHost host = new HostBuilder()
.ConfigureDefaultTestHost<BindToBlobClientProgram>(prog, builder =>
{
builder.AddAzureStorageBlobs()
.UseStorageServices(blobServiceClient, queueServiceClient);
})
.Build();

var jobHost = host.GetJobHost<BindToBlobClientProgram>();
await jobHost.CallAsync(nameof(BindToBlobClientProgram.Run));

var result = prog.Result;

// Assert
Assert.NotNull(result);
Assert.AreEqual(BlobName, result.Name);
Assert.NotNull(result.BlobContainerName);
Assert.AreEqual(ContainerName, result.BlobContainerName);
var container = GetContainerReference(blobServiceClient, ContainerName);
Assert.True(await container.ExistsAsync());
var blob = container.GetBlockBlobClient(BlobName);
Assert.False(await blob.ExistsAsync());
}

[Test]
public async Task Blob_IfBoundToTextWriter_CreatesBlob()
{
Expand Down Expand Up @@ -123,6 +152,17 @@ public void Run(
}
}

private class BindToBlobClientProgram
{
public BlobClient Result { get; set; }

public void Run(
[Blob(BlobPath)] BlobClient blob)
{
this.Result = blob;
}
}

private class BindToTextWriterProgram
{
public static void Run([QueueTrigger(TriggerQueueName)] string message,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ private class BindToCloudBlobProgram
{
public static TaskCompletionSource<BlobBaseClient> TaskSource { get; set; }

public static void Run([BlobTrigger(BlobPath)] BlobBaseClient blob) // TODO (kasobol-msft how about binding to BlobClient??
public static void Run([BlobTrigger(BlobPath)] BlobBaseClient blob)
{
TaskSource.TrySetResult(blob);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,19 @@ public async Task Blob_IfBoundToCloudBlockBlob_CanCall()
await CallAsync(typeof(BlobProgram), "BindToCloudBlockBlob");
}

[Test]
public async Task Blob_IfBoundToBlobClient_CanCall()
{
// Arrange
var container = blobServiceClient.GetBlobContainerClient(ContainerName);
var inputBlob = container.GetBlockBlobClient(BlobName);
await container.CreateIfNotExistsAsync();
await inputBlob.UploadTextAsync("ignore");

// Act
await CallAsync(typeof(BlobProgram), "BindToBlobClient");
}

[Test]
public async Task Blob_IfBoundToString_CanCall()
{
Expand Down Expand Up @@ -281,6 +294,50 @@ public static void Call([BlobTrigger(BlobPath)] BlockBlobClient blob)
}
}

[Test]
public async Task BlobTrigger_IfBoundToBlobClient_CanCall()
{
// Arrange
var container = blobServiceClient.GetBlobContainerClient(ContainerName);
var blob = container.GetBlockBlobClient(BlobName);
await container.CreateIfNotExistsAsync();
await blob.UploadTextAsync("ignore");

// TODO: Remove argument once host.Call supports more flexibility.
IDictionary<string, object> arguments = new Dictionary<string, object>
{
{ "blob", BlobPath }
};

// Act
var result = await CallAsync<BlobClient>(typeof(BlobTriggerBindToBlobClientProgram),
"Call", arguments, (s) => BlobTriggerBindToBlobClientProgram.TaskSource = s);

// Assert
Assert.NotNull(result);
}

[Test]
public async Task BlobTrigger_IfBoundToBlobClientAndTriggerArgumentIsMissing_CallThrows()
{
// Act
Exception exception = await CallFailureAsync(typeof(BlobTriggerBindToBlobClientProgram), "Call");

// Assert
Assert.IsInstanceOf<InvalidOperationException>(exception);
Assert.AreEqual("Missing value for trigger parameter 'blob'.", exception.Message);
}

private class BlobTriggerBindToBlobClientProgram
{
public static TaskCompletionSource<BlobClient> TaskSource { get; set; }

public static void Call([BlobTrigger(BlobPath)] BlobClient blob)
{
TaskSource.TrySetResult(blob);
}
}

[Test]
public async Task BlobTrigger_IfBoundToCloudPageBlob_CanCall()
{
Expand Down Expand Up @@ -606,6 +663,12 @@ public static void BindToCloudBlockBlob([Blob(BlobPath)] BlockBlobClient blob)
Assert.AreEqual(BlobName, blob.Name);
}

public static void BindToBlobClient([Blob(BlobPath)] BlobClient blob)
{
Assert.NotNull(blob);
Assert.AreEqual(BlobName, blob.Name);
}

public static void BindToString([Blob(BlobPath)] string content)
{
Assert.NotNull(content);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,14 @@ public async Task BindToIEnumerableICloudBlob()
Assert.AreEqual(6, _numBlobsRead);
}

[Test]
public async Task BindToIEnumerableBlobClient()
{
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("IEnumerableBlobClientBinding"));

Assert.AreEqual(6, _numBlobsRead);
}

[Test]
public async Task BindToByteArray()
{
Expand Down Expand Up @@ -514,6 +522,22 @@ public static async Task IEnumerableICloudBlobBinding(
_numBlobsRead = blobs.Count();
}

[NoAutomaticTrigger]
public static async Task IEnumerableBlobClientBinding(
[Blob(ContainerName)] IEnumerable<BlobClient> blobs)
{
foreach (var blob in blobs)
{
Stream stream = await blob.OpenReadAsync();
using (StreamReader reader = new StreamReader(stream))
{
string content = reader.ReadToEnd();
Assert.AreEqual(TestData, content);
}
}
_numBlobsRead = blobs.Count();
}

[NoAutomaticTrigger]
public static void StringBinding_Block(
[Blob(ContainerName + "/blob1")] string blob)
Expand Down

0 comments on commit 2a5c344

Please sign in to comment.