Skip to content

Commit c435dda

Browse files
Blob converter for ParameterBindingData support (#1108)
Co-authored-by: Surgupta <[email protected]>
1 parent 88f7bfb commit c435dda

File tree

15 files changed

+588
-20
lines changed

15 files changed

+588
-20
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using Microsoft.Extensions.Configuration;
5+
6+
namespace Microsoft.Azure.Functions.Worker.Extensions
7+
{
8+
internal static class ConfigurationExtensions
9+
{
10+
private const string WebJobsConfigurationSectionName = "AzureWebJobs";
11+
private const string ConnectionStringsConfigurationSectionName = "ConnectionStrings";
12+
13+
/// <summary>
14+
/// Gets the configuration section for a given connection name.
15+
/// </summary>
16+
/// <param name="configuration">The configuration.</param>
17+
/// <param name="connectionName">The connection string key.</param>
18+
internal static IConfigurationSection GetWebJobsConnectionStringSection(this IConfiguration configuration, string connectionName)
19+
{
20+
// first try prefixing
21+
string prefixedConnectionStringName = GetPrefixedConnectionStringName(connectionName);
22+
IConfigurationSection section = configuration.GetConnectionStringOrSetting(prefixedConnectionStringName);
23+
24+
if (!section.Exists())
25+
{
26+
// next try a direct un-prefixed lookup
27+
section = configuration.GetConnectionStringOrSetting(connectionName);
28+
}
29+
30+
return section;
31+
}
32+
33+
/// <summary>
34+
/// Creates a WebJobs specific prefixed string using a given connection name.
35+
/// </summary>
36+
/// <param name="connectionName">The connection string key.</param>
37+
private static string GetPrefixedConnectionStringName(string connectionName)
38+
{
39+
return WebJobsConfigurationSectionName + connectionName;
40+
}
41+
42+
/// <summary>
43+
/// Looks for a connection string by first checking the ConfigurationStrings section, and then the root.
44+
/// </summary>
45+
/// <param name="configuration">The configuration.</param>
46+
/// <param name="connectionName">The connection string key.</param>
47+
private static IConfigurationSection GetConnectionStringOrSetting(this IConfiguration configuration, string connectionName)
48+
{
49+
if (configuration.GetSection(ConnectionStringsConfigurationSectionName).Exists())
50+
{
51+
IConfigurationSection onConnectionStrings = configuration.GetSection(ConnectionStringsConfigurationSectionName).GetSection(connectionName);
52+
if (onConnectionStrings.Exists())
53+
{
54+
return onConnectionStrings;
55+
}
56+
}
57+
58+
return configuration.GetSection(connectionName);
59+
}
60+
}
61+
}
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
### Release notes
22
<!-- Please add your release notes in the following format:
33
- My change description (#PR/#issue)
4-
-->
4+
-->
5+
6+
- Add support for SDK-type bindings via deferred binding feature #1108
7+
- Assign cardinality correctly for Blob collection scenarios #1271

extensions/Worker.Extensions.Storage.Blobs/src/BlobInputAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4-
using System;
54
using Microsoft.Azure.Functions.Worker.Extensions.Abstractions;
65

76
namespace Microsoft.Azure.Functions.Worker
87
{
8+
[SupportsDeferredBinding]
99
public sealed class BlobInputAttribute : InputBindingAttribute
1010
{
1111
private readonly string _blobPath;
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Reflection;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using Azure.Storage.Blobs;
12+
using Azure.Storage.Blobs.Specialized;
13+
using Microsoft.Azure.Functions.Worker.Converters;
14+
using Microsoft.Azure.Functions.Worker.Core;
15+
using Microsoft.Extensions.Options;
16+
using Microsoft.Extensions.Logging;
17+
18+
namespace Microsoft.Azure.Functions.Worker
19+
{
20+
/// <summary>
21+
/// Converter to bind Blob Storage type parameters.
22+
/// </summary>
23+
internal class BlobStorageConverter : IInputConverter
24+
{
25+
private readonly IOptions<WorkerOptions> _workerOptions;
26+
private readonly IOptionsSnapshot<BlobStorageBindingOptions> _blobOptions;
27+
28+
private readonly ILogger<BlobStorageConverter> _logger;
29+
30+
public BlobStorageConverter(IOptions<WorkerOptions> workerOptions, IOptionsSnapshot<BlobStorageBindingOptions> blobOptions, ILogger<BlobStorageConverter> logger)
31+
{
32+
_workerOptions = workerOptions ?? throw new ArgumentNullException(nameof(workerOptions));
33+
_blobOptions = blobOptions ?? throw new ArgumentNullException(nameof(blobOptions));
34+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
35+
}
36+
37+
public async ValueTask<ConversionResult> ConvertAsync(ConverterContext context)
38+
{
39+
return context?.Source switch
40+
{
41+
ModelBindingData binding => await ConvertFromBindingDataAsync(context, binding),
42+
CollectionModelBindingData binding => await ConvertFromCollectionBindingDataAsync(context, binding),
43+
_ => ConversionResult.Unhandled(),
44+
};
45+
}
46+
47+
private async ValueTask<ConversionResult> ConvertFromBindingDataAsync(ConverterContext context, ModelBindingData modelBindingData)
48+
{
49+
if (!IsBlobExtension(modelBindingData))
50+
{
51+
return ConversionResult.Unhandled();
52+
}
53+
54+
try
55+
{
56+
Dictionary<string, string> content = GetBindingDataContent(modelBindingData);
57+
var result = await ConvertModelBindingDataAsync(content, context.TargetType, modelBindingData);
58+
59+
if (result is not null)
60+
{
61+
return ConversionResult.Success(result);
62+
}
63+
}
64+
catch (Exception ex)
65+
{
66+
return ConversionResult.Failed(ex);
67+
}
68+
69+
return ConversionResult.Unhandled();
70+
}
71+
72+
private async ValueTask<ConversionResult> ConvertFromCollectionBindingDataAsync(ConverterContext context, CollectionModelBindingData collectionModelBindingData)
73+
{
74+
var blobCollection = new List<object>(collectionModelBindingData.ModelBindingDataArray.Length);
75+
Type elementType = context.TargetType.IsArray ? context.TargetType.GetElementType() : context.TargetType.GenericTypeArguments[0];
76+
77+
try
78+
{
79+
foreach (ModelBindingData modelBindingData in collectionModelBindingData.ModelBindingDataArray)
80+
{
81+
if (!IsBlobExtension(modelBindingData))
82+
{
83+
return ConversionResult.Unhandled();
84+
}
85+
86+
Dictionary<string, string> content = GetBindingDataContent(modelBindingData);
87+
var element = await ConvertModelBindingDataAsync(content, elementType, modelBindingData);
88+
89+
if (element is not null)
90+
{
91+
blobCollection.Add(element);
92+
}
93+
}
94+
95+
var methodName = context.TargetType.IsArray ? nameof(CloneToArray) : nameof(CloneToList);
96+
var result = ToTargetTypeCollection(blobCollection, methodName, elementType);
97+
98+
return ConversionResult.Success(result);
99+
}
100+
catch (Exception ex)
101+
{
102+
return ConversionResult.Failed(ex);
103+
}
104+
}
105+
106+
private bool IsBlobExtension(ModelBindingData bindingData)
107+
{
108+
if (bindingData?.Source is not Constants.BlobExtensionName)
109+
{
110+
_logger.LogTrace("Source '{source}' is not supported by {converter}", bindingData?.Source, nameof(BlobStorageConverter));
111+
return false;
112+
}
113+
114+
return true;
115+
}
116+
117+
private Dictionary<string, string> GetBindingDataContent(ModelBindingData bindingData)
118+
{
119+
return bindingData?.ContentType switch
120+
{
121+
Constants.JsonContentType => new Dictionary<string, string>(bindingData?.Content?.ToObjectFromJson<Dictionary<string, string>>(), StringComparer.OrdinalIgnoreCase),
122+
_ => throw new NotSupportedException($"Unexpected content-type. Currently only {Constants.JsonContentType} is supported.")
123+
};
124+
}
125+
126+
private async Task<object?> ConvertModelBindingDataAsync(IDictionary<string, string> content, Type targetType, ModelBindingData bindingData)
127+
{
128+
content.TryGetValue(Constants.Connection, out var connectionName);
129+
content.TryGetValue(Constants.ContainerName, out var containerName);
130+
content.TryGetValue(Constants.BlobName, out var blobName);
131+
132+
if (string.IsNullOrEmpty(connectionName) || string.IsNullOrEmpty(containerName))
133+
{
134+
throw new ArgumentNullException("'Connection' and 'ContainerName' cannot be null or empty");
135+
}
136+
137+
return await ToTargetTypeAsync(targetType, connectionName, containerName, blobName);
138+
}
139+
140+
private async Task<object?> ToTargetTypeAsync(Type targetType, string connectionName, string containerName, string blobName) => targetType switch
141+
{
142+
Type _ when targetType == typeof(String) => await GetBlobStringAsync(connectionName, containerName, blobName),
143+
Type _ when targetType == typeof(Stream) => await GetBlobStreamAsync(connectionName, containerName, blobName),
144+
Type _ when targetType == typeof(Byte[]) => await GetBlobBinaryDataAsync(connectionName, containerName, blobName),
145+
Type _ when targetType == typeof(BlobBaseClient) => CreateBlobClient<BlobBaseClient>(connectionName, containerName, blobName),
146+
Type _ when targetType == typeof(BlobClient) => CreateBlobClient<BlobClient>(connectionName, containerName, blobName),
147+
Type _ when targetType == typeof(BlockBlobClient) => CreateBlobClient<BlockBlobClient>(connectionName, containerName, blobName),
148+
Type _ when targetType == typeof(PageBlobClient) => CreateBlobClient<PageBlobClient>(connectionName, containerName, blobName),
149+
Type _ when targetType == typeof(AppendBlobClient) => CreateBlobClient<AppendBlobClient>(connectionName, containerName, blobName),
150+
Type _ when targetType == typeof(BlobContainerClient) => CreateBlobContainerClient(connectionName, containerName),
151+
_ => await DeserializeToTargetObjectAsync(targetType, connectionName, containerName, blobName)
152+
};
153+
154+
private async Task<object?> DeserializeToTargetObjectAsync(Type targetType, string connectionName, string containerName, string blobName)
155+
{
156+
var content = await GetBlobStreamAsync(connectionName, containerName, blobName);
157+
return _workerOptions?.Value?.Serializer?.Deserialize(content, targetType, CancellationToken.None);
158+
}
159+
160+
private object? ToTargetTypeCollection(IEnumerable<object> blobCollection, string methodName, Type type)
161+
{
162+
blobCollection = blobCollection.Select(b => Convert.ChangeType(b, type));
163+
MethodInfo method = typeof(BlobStorageConverter).GetMethod(methodName, BindingFlags.Static | BindingFlags.NonPublic);
164+
MethodInfo genericMethod = method.MakeGenericMethod(type);
165+
166+
return genericMethod.Invoke(null, new[] { blobCollection.ToList() });
167+
}
168+
169+
private static T[] CloneToArray<T>(IList<object> source)
170+
{
171+
return source.Cast<T>().ToArray();
172+
}
173+
174+
private static IEnumerable<T> CloneToList<T>(IList<object> source)
175+
{
176+
return source.Cast<T>();
177+
}
178+
179+
private async Task<string> GetBlobStringAsync(string connectionName, string containerName, string blobName)
180+
{
181+
var client = CreateBlobClient<BlobClient>(connectionName, containerName, blobName);
182+
return await GetBlobContentStringAsync(client);
183+
}
184+
185+
private async Task<string> GetBlobContentStringAsync(BlobClient client)
186+
{
187+
var download = await client.DownloadContentAsync();
188+
return download.Value.Content.ToString();
189+
}
190+
191+
private async Task<Byte[]> GetBlobBinaryDataAsync(string connectionName, string containerName, string blobName)
192+
{
193+
using MemoryStream stream = new();
194+
var client = CreateBlobClient<BlobClient>(connectionName, containerName, blobName);
195+
await client.DownloadToAsync(stream);
196+
return stream.ToArray();
197+
}
198+
199+
private async Task<Stream> GetBlobStreamAsync(string connectionName, string containerName, string blobName)
200+
{
201+
var client = CreateBlobClient<BlobClient>(connectionName, containerName, blobName);
202+
var download = await client.DownloadStreamingAsync();
203+
return download.Value.Content;
204+
}
205+
206+
private BlobContainerClient CreateBlobContainerClient(string connectionName, string containerName)
207+
{
208+
var blobStorageOptions = _blobOptions.Get(connectionName);
209+
BlobServiceClient blobServiceClient = blobStorageOptions.CreateClient();
210+
BlobContainerClient container = blobServiceClient.GetBlobContainerClient(containerName);
211+
return container;
212+
}
213+
214+
private T CreateBlobClient<T>(string connectionName, string containerName, string blobName) where T : BlobBaseClient
215+
{
216+
if (string.IsNullOrEmpty(blobName))
217+
{
218+
throw new ArgumentNullException(nameof(blobName));
219+
}
220+
221+
BlobContainerClient container = CreateBlobContainerClient(connectionName, containerName);
222+
223+
Type targetType = typeof(T);
224+
BlobBaseClient blobClient = targetType switch
225+
{
226+
Type _ when targetType == typeof(BlobClient) => container.GetBlobClient(blobName),
227+
Type _ when targetType == typeof(BlockBlobClient) => container.GetBlockBlobClient(blobName),
228+
Type _ when targetType == typeof(PageBlobClient) => container.GetPageBlobClient(blobName),
229+
Type _ when targetType == typeof(AppendBlobClient) => container.GetAppendBlobClient(blobName),
230+
_ => container.GetBlobBaseClient(blobName)
231+
};
232+
233+
return (T)blobClient;
234+
}
235+
}
236+
}

extensions/Worker.Extensions.Storage.Blobs/src/BlobTriggerAttribute.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
namespace Microsoft.Azure.Functions.Worker
77
{
8+
[SupportsDeferredBinding]
89
public sealed class BlobTriggerAttribute : TriggerBindingAttribute
910
{
1011
private readonly string _blobPath;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Azure.Core;
6+
using Azure.Storage.Blobs;
7+
8+
namespace Microsoft.Azure.Functions.Worker
9+
{
10+
internal class BlobStorageBindingOptions
11+
{
12+
public string? ConnectionString { get; set; }
13+
14+
public Uri? ServiceUri { get; set; }
15+
16+
public TokenCredential? Credential { get; set; }
17+
18+
public BlobClientOptions? BlobClientOptions { get; set; }
19+
20+
public BlobServiceClient CreateClient()
21+
{
22+
if (ServiceUri is not null && Credential is not null)
23+
{
24+
return new BlobServiceClient(ServiceUri, Credential, BlobClientOptions);
25+
}
26+
27+
return new BlobServiceClient(ConnectionString, BlobClientOptions);
28+
}
29+
}
30+
}

0 commit comments

Comments
 (0)