-
Notifications
You must be signed in to change notification settings - Fork 204
Cosmos DB converter for SDK-type support and samples #1109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
4d38485
cosmos db converter and samples
liliankasem 8f9fc3a
Add collection and managed identity support. Try using cosmos v3
liliankasem ce9318a
Switch to v3
liliankasem d3912ac
Refactor and support collections
liliankasem 54d6758
update samples
liliankasem af1d00e
Update samples
liliankasem 1dfc2c2
cleaning up
liliankasem f0d4087
Refactor to use IConfig and AzureComponentFactory
liliankasem 0addad6
resolve sqlquery params
liliankasem 98cbc7c
adding IOptions p1
liliankasem 3a2b30e
cleanup
liliankasem 9be6e88
woops
liliankasem cd37c65
cleanup factory and ioptions
liliankasem File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
55 changes: 55 additions & 0 deletions
55
extensions/Worker.Extensions.CosmosDB/src/Config/ConfigurationExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| } | ||
| } |
29 changes: 29 additions & 0 deletions
29
extensions/Worker.Extensions.CosmosDB/src/Config/CosmosDBBindingOptions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| } | ||
| } |
56 changes: 56 additions & 0 deletions
56
extensions/Worker.Extensions.CosmosDB/src/Config/CosmosDBBindingOptionsSetup.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 " + | ||
| $"string representing a connection string."); | ||
| } | ||
|
|
||
| options.Credential = _componentFactory.CreateTokenCredential(connectionSection); | ||
| } | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"; | ||
|
jviau marked this conversation as resolved.
|
||
| internal const string AccountEndpoint = "accountEndpoint"; | ||
| } | ||
| } | ||
210 changes: 210 additions & 0 deletions
210
extensions/Worker.Extensions.CosmosDB/src/CosmosDBConverter.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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? | ||
|
liliankasem marked this conversation as resolved.
|
||
| Console.WriteLine(ex); | ||
|
|
||
| if (ex is CosmosException docEx) | ||
|
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() | ||
|
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; | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.