Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using Microsoft.Extensions.Configuration;

namespace Microsoft.Azure.Functions.Worker
{
internal static class ConfigurationExtensions
{
public static IConfigurationSection GetCosmosConnectionStringSection(this IConfiguration configuration, string connectionStringName)
{
if (string.IsNullOrWhiteSpace(connectionStringName))
{
connectionStringName = Constants.ExtensionName; // default
}

// first try prefixing
string prefixedConnectionStringName = GetPrefixedConnectionStringName(connectionStringName);
IConfigurationSection section = configuration.GetConnectionStringOrSetting(prefixedConnectionStringName);

if (!section.Exists())
{
// next try a direct unprefixed lookup
section = configuration.GetConnectionStringOrSetting(connectionStringName);
}

return section;
}

public static string GetPrefixedConnectionStringName(string connectionStringName)
{
return Constants.ConfigurationSectionName + connectionStringName;
}

/// <summary>
/// Looks for a connection string by first checking the ConfigurationStrings section, and then the root.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="connectionName">The connection string key.</param>
/// <returns></returns>
public static IConfigurationSection GetConnectionStringOrSetting(this IConfiguration configuration, string connectionName)
{
if (configuration.GetSection(Constants.ConnectionStringsSectionName).Exists())
{
IConfigurationSection onConnectionStrings = configuration.GetSection(Constants.ConnectionStringsSectionName).GetSection(connectionName);
if (onConnectionStrings.Exists())
{
return onConnectionStrings;
}
}

return configuration.GetSection(connectionName);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using Azure.Core;
using Microsoft.Azure.Cosmos;

namespace Microsoft.Azure.Functions.Worker
{
internal class CosmosDBBindingOptions
{
public string? ConnectionString { get; set; }

public string? AccountEndpoint { get; set; }

public TokenCredential? Credential { get; set; }

public CosmosClient CreateClient(CosmosClientOptions cosmosClientOptions)
{
if (string.IsNullOrEmpty(ConnectionString))
{
// AAD auth
return new CosmosClient(AccountEndpoint, Credential, cosmosClientOptions);
}

// Connection string based auth
return new CosmosClient(ConnectionString, cosmosClientOptions);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// 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.Extensions.Azure;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;

namespace Microsoft.Azure.Functions.Worker
{
internal class CosmosDBBindingOptionsSetup : IConfigureNamedOptions<CosmosDBBindingOptions>
{
private readonly IConfiguration _configuration;
private readonly AzureComponentFactory _componentFactory;

public CosmosDBBindingOptionsSetup(IConfiguration configuration, AzureComponentFactory componentFactory)
{
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
_componentFactory = componentFactory ?? throw new ArgumentNullException(nameof(componentFactory));
}

public void Configure(CosmosDBBindingOptions options)
{
Configure(Options.DefaultName, options);
}

public void Configure(string name, CosmosDBBindingOptions options)
{
IConfigurationSection connectionSection = _configuration.GetCosmosConnectionStringSection(name);

if (!connectionSection.Exists())
{
// Not found
throw new InvalidOperationException($"Cosmos DB 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
{
options.AccountEndpoint = connectionSection[Constants.AccountEndpoint];
if (string.IsNullOrWhiteSpace(options.AccountEndpoint))
{
// Not found
throw new InvalidOperationException($"Connection should have an '{Constants.AccountEndpoint}' property or be a " +
Comment thread
liliankasem marked this conversation as resolved.
$"string representing a connection string.");
}

options.Credential = _componentFactory.CreateTokenCredential(connectionSection);
}
}
}
}
14 changes: 14 additions & 0 deletions extensions/Worker.Extensions.CosmosDB/src/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

namespace Microsoft.Azure.Functions.Worker
{
internal static class Constants
{
internal const string ExtensionName = "CosmosDB";
internal const string ConfigurationSectionName = "AzureWebJobs";
internal const string ConnectionStringsSectionName = "ConnectionStrings";
Comment thread
jviau marked this conversation as resolved.
internal const string AccountEndpoint = "accountEndpoint";
}
}
210 changes: 210 additions & 0 deletions extensions/Worker.Extensions.CosmosDB/src/CosmosDBConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// 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.Threading.Tasks;
using Microsoft.Azure.Functions.Worker.Core;
using Microsoft.Azure.Functions.Worker.Converters;
using System.Collections.Generic;
using Microsoft.Azure.Cosmos;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Options;

namespace Microsoft.Azure.Functions.Worker
{
/// <summary>
/// Converter to bind Cosmos DB type parameters.
/// </summary>
internal class CosmosDBConverter : IInputConverter
{
private readonly IOptionsSnapshot<CosmosDBBindingOptions> _cosmosOptions;

public CosmosDBConverter(IOptionsSnapshot<CosmosDBBindingOptions> cosmosOptions)
{
_cosmosOptions = cosmosOptions ?? throw new ArgumentNullException(nameof(cosmosOptions));
}

public async ValueTask<ConversionResult> ConvertAsync(ConverterContext context)
{
if (context.Source is ModelBindingData modelBindingData)
{
if (modelBindingData.Source is not Constants.ExtensionName)
{
return ConversionResult.Unhandled();
}

try
{
var cosmosAttribute = modelBindingData.Content.ToObjectFromJson<CosmosDBInputAttribute>();
object result = await ToTargetType(context.TargetType, cosmosAttribute);

if (result is not null)
{
return ConversionResult.Success(result);
}
}
catch (Exception ex)
{
// What do we want to do for error handling?
Comment thread
liliankasem marked this conversation as resolved.
Console.WriteLine(ex);

if (ex is CosmosException docEx)
Comment thread
liliankasem marked this conversation as resolved.
{
throw;
}
}
}

if (context.Source is CollectionModelBindingData collectionModelBindingData)
{
if (collectionModelBindingData.ModelBindingDataArray.Any(x => x.Source is Constants.ExtensionName))
{
try
{
var collectionResult = await ToTargetTypeCollection(context, context.TargetType, collectionModelBindingData);

if (collectionResult is not null && collectionResult is { Count: > 0 })
{
return ConversionResult.Success(collectionResult);
}
}
catch (Exception ex)
{
// TODO: DeserializeObject could throw
Console.WriteLine(ex);
}
}
}

return ConversionResult.Unhandled();
}

private async Task<object> ToTargetType(Type targetType, CosmosDBInputAttribute cosmosAttribute) => targetType switch
{
Type _ when targetType == typeof(CosmosClient) => CreateCosmosClient<CosmosClient>(cosmosAttribute),
Type _ when targetType == typeof(Database) => CreateCosmosClient<Database>(cosmosAttribute),
Type _ when targetType == typeof(Container) => CreateCosmosClient<Container>(cosmosAttribute),
_ => await CreateTargetObject(targetType, cosmosAttribute)
};

private async Task<List<object>> ToTargetTypeCollection(ConverterContext context, Type targetType, CollectionModelBindingData collectionModelBindingData)
{
var collectionCosmosItems = new List<object>(collectionModelBindingData.ModelBindingDataArray.Length);

foreach (ModelBindingData modelBindingData in collectionModelBindingData.ModelBindingDataArray)
{
var cosmosAttribute = modelBindingData.Content.ToObjectFromJson<CosmosDBInputAttribute>();
var cosmosItem = await ToTargetType(targetType, cosmosAttribute);
if (cosmosItem is not null)
{
collectionCosmosItems.Add(cosmosItem);
}
}

return collectionCosmosItems;
}

private async Task<object> CreateTargetObject(Type targetType, CosmosDBInputAttribute cosmosAttribute)
{
// if target type is a collection and NOT of type IList, should we handle this early
// and let users know we only support IList types?

if (targetType.GenericTypeArguments.Any())
{
targetType = targetType.GenericTypeArguments.FirstOrDefault();
}

MethodInfo createPOCOFromReferenceMethod = GetType()
Comment thread
jviau marked this conversation as resolved.
.GetMethod(nameof(CreatePOCOFromReference), BindingFlags.Instance | BindingFlags.NonPublic)
.MakeGenericMethod(new Type[] { targetType });

return await (Task<object>)createPOCOFromReferenceMethod.Invoke(this, new object[] { cosmosAttribute });
}

// This will be for input bindings only.
// a) If users bind to just a POCO, they need to provide the `Id` and `PartitionKey`
// attributes so that we know which document to pull
// b) If they bind to IList<POCO>, we should be able to just pull every document
// in the container, unless they specify the the SqlQuery attribute, in which case
// we need to filter on that.
private async Task<object> CreatePOCOFromReference<T>(CosmosDBInputAttribute cosmosAttribute)
{
var container = CreateCosmosClient<Container>(cosmosAttribute) as Container;

if (container is null)
{
// use proper exception type or handle
throw new InvalidOperationException("Houston, we have a problem");
}

var partitionKey = cosmosAttribute.PartitionKey == null ? PartitionKey.None : new PartitionKey(cosmosAttribute.PartitionKey);

if (cosmosAttribute.Id is not null)
{
ItemResponse<T> item = await container.ReadItemAsync<T>(cosmosAttribute.Id, partitionKey);

if (item is null || item?.StatusCode is not System.Net.HttpStatusCode.OK)
{
throw new InvalidOperationException($"Unable to retrieve document with ID {cosmosAttribute.Id} and PartitionKey {cosmosAttribute.PartitionKey}");
}

return item.Resource;
}

QueryDefinition queryDefinition = null;
if (cosmosAttribute.SqlQuery is not null)
{
queryDefinition = new QueryDefinition(cosmosAttribute.SqlQuery);
if (cosmosAttribute.SqlQueryParameters != null)
{
// TODO: fix SqlQueryParameters being empty
foreach (var parameter in cosmosAttribute.SqlQueryParameters)
{
queryDefinition.WithParameter(parameter.Item1, parameter.Item2);
}
}
}

QueryRequestOptions queryRequestOptions = new() { PartitionKey = partitionKey };
using (var iterator = container.GetItemQueryIterator<T>(queryDefinition: queryDefinition, requestOptions: queryRequestOptions))
{
return await ExtractCosmosDocuments(iterator);
}
}

private async Task<IList<T>> ExtractCosmosDocuments<T>(FeedIterator<T> iterator)
{
var documentList = new List<T>();
while (iterator.HasMoreResults)
{
FeedResponse<T> response = await iterator.ReadNextAsync();
documentList.AddRange(response.Resource);
}
return documentList;
}

private T CreateCosmosClient<T>(CosmosDBInputAttribute cosmosAttribute)
{
if (cosmosAttribute is null)
{
// What do?
throw new InvalidOperationException("Cosmos attribute cannot be null");
}

var cosmosDBOptions = _cosmosOptions.Get(cosmosAttribute.Connection);
CosmosClientOptions cosmosClientOptions = new() { ApplicationPreferredRegions = Utilities.ParsePreferredLocations(cosmosAttribute.PreferredLocations) };
CosmosClient cosmosClient = cosmosDBOptions.CreateClient(cosmosClientOptions);

Type targetType = typeof(T);
object cosmosReference = targetType switch
{
Type _ when targetType == typeof(Database) => cosmosClient.GetDatabase(cosmosAttribute.DatabaseName),
Type _ when targetType == typeof(Container) => cosmosClient.GetContainer(cosmosAttribute.DatabaseName, cosmosAttribute.ContainerName),
_ => cosmosClient
};

return (T)cosmosReference;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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 Microsoft.Azure.Functions.Worker.Extensions.Abstractions;

namespace Microsoft.Azure.Functions.Worker
Expand Down Expand Up @@ -44,7 +45,7 @@ public CosmosDBInputAttribute(string databaseName, string containerName)

/// <summary>
/// Optional.
/// When specified on an output binding and <see cref="CreateIfNotExists"/> is true, defines the partition key
/// When specified on an output binding and <see cref="CreateIfNotExists"/> is true, defines the partition key
/// path for the created container.
/// When specified on an input binding, specifies the partition key value for the lookup.
/// May include binding parameters.
Expand All @@ -67,5 +68,7 @@ public CosmosDBInputAttribute(string databaseName, string containerName)
/// PreferredLocations = "East US,South Central US,North Europe"
/// </example>
public string? PreferredLocations { get; set; }

internal IEnumerable<(string, object)> SqlQueryParameters { get; set; }
}
}
Loading