diff --git a/extensions/Worker.Extensions.CosmosDB/src/Config/ConfigurationExtensions.cs b/extensions/Worker.Extensions.CosmosDB/src/Config/ConfigurationExtensions.cs new file mode 100644 index 000000000..8dbb396c2 --- /dev/null +++ b/extensions/Worker.Extensions.CosmosDB/src/Config/ConfigurationExtensions.cs @@ -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; + } + + /// + /// Looks for a connection string by first checking the ConfigurationStrings section, and then the root. + /// + /// The configuration. + /// The connection string key. + /// + 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); + } + } +} \ No newline at end of file diff --git a/extensions/Worker.Extensions.CosmosDB/src/Config/CosmosDBBindingOptions.cs b/extensions/Worker.Extensions.CosmosDB/src/Config/CosmosDBBindingOptions.cs new file mode 100644 index 000000000..fa2f86caf --- /dev/null +++ b/extensions/Worker.Extensions.CosmosDB/src/Config/CosmosDBBindingOptions.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/extensions/Worker.Extensions.CosmosDB/src/Config/CosmosDBBindingOptionsSetup.cs b/extensions/Worker.Extensions.CosmosDB/src/Config/CosmosDBBindingOptionsSetup.cs new file mode 100644 index 000000000..f3133620d --- /dev/null +++ b/extensions/Worker.Extensions.CosmosDB/src/Config/CosmosDBBindingOptionsSetup.cs @@ -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 + { + 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); + } + } + } +} \ No newline at end of file diff --git a/extensions/Worker.Extensions.CosmosDB/src/Constants.cs b/extensions/Worker.Extensions.CosmosDB/src/Constants.cs new file mode 100644 index 000000000..f5e59ea3d --- /dev/null +++ b/extensions/Worker.Extensions.CosmosDB/src/Constants.cs @@ -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"; + internal const string AccountEndpoint = "accountEndpoint"; + } +} \ No newline at end of file diff --git a/extensions/Worker.Extensions.CosmosDB/src/CosmosDBConverter.cs b/extensions/Worker.Extensions.CosmosDB/src/CosmosDBConverter.cs new file mode 100644 index 000000000..b1313136d --- /dev/null +++ b/extensions/Worker.Extensions.CosmosDB/src/CosmosDBConverter.cs @@ -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 +{ + /// + /// Converter to bind Cosmos DB type parameters. + /// + internal class CosmosDBConverter : IInputConverter + { + private readonly IOptionsSnapshot _cosmosOptions; + + public CosmosDBConverter(IOptionsSnapshot cosmosOptions) + { + _cosmosOptions = cosmosOptions ?? throw new ArgumentNullException(nameof(cosmosOptions)); + } + + public async ValueTask 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(); + 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? + Console.WriteLine(ex); + + if (ex is CosmosException docEx) + { + 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 ToTargetType(Type targetType, CosmosDBInputAttribute cosmosAttribute) => targetType switch + { + Type _ when targetType == typeof(CosmosClient) => CreateCosmosClient(cosmosAttribute), + Type _ when targetType == typeof(Database) => CreateCosmosClient(cosmosAttribute), + Type _ when targetType == typeof(Container) => CreateCosmosClient(cosmosAttribute), + _ => await CreateTargetObject(targetType, cosmosAttribute) + }; + + private async Task> ToTargetTypeCollection(ConverterContext context, Type targetType, CollectionModelBindingData collectionModelBindingData) + { + var collectionCosmosItems = new List(collectionModelBindingData.ModelBindingDataArray.Length); + + foreach (ModelBindingData modelBindingData in collectionModelBindingData.ModelBindingDataArray) + { + var cosmosAttribute = modelBindingData.Content.ToObjectFromJson(); + var cosmosItem = await ToTargetType(targetType, cosmosAttribute); + if (cosmosItem is not null) + { + collectionCosmosItems.Add(cosmosItem); + } + } + + return collectionCosmosItems; + } + + private async Task 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() + .GetMethod(nameof(CreatePOCOFromReference), BindingFlags.Instance | BindingFlags.NonPublic) + .MakeGenericMethod(new Type[] { targetType }); + + return await (Task)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, 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 CreatePOCOFromReference(CosmosDBInputAttribute cosmosAttribute) + { + var container = CreateCosmosClient(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 item = await container.ReadItemAsync(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(queryDefinition: queryDefinition, requestOptions: queryRequestOptions)) + { + return await ExtractCosmosDocuments(iterator); + } + } + + private async Task> ExtractCosmosDocuments(FeedIterator iterator) + { + var documentList = new List(); + while (iterator.HasMoreResults) + { + FeedResponse response = await iterator.ReadNextAsync(); + documentList.AddRange(response.Resource); + } + return documentList; + } + + private T CreateCosmosClient(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; + } + } +} diff --git a/extensions/Worker.Extensions.CosmosDB/src/CosmosDBInputAttribute.cs b/extensions/Worker.Extensions.CosmosDB/src/CosmosDBInputAttribute.cs index 895ec9e9b..218de589c 100644 --- a/extensions/Worker.Extensions.CosmosDB/src/CosmosDBInputAttribute.cs +++ b/extensions/Worker.Extensions.CosmosDB/src/CosmosDBInputAttribute.cs @@ -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 @@ -44,7 +45,7 @@ public CosmosDBInputAttribute(string databaseName, string containerName) /// /// Optional. - /// When specified on an output binding and is true, defines the partition key + /// When specified on an output binding and 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. @@ -67,5 +68,7 @@ public CosmosDBInputAttribute(string databaseName, string containerName) /// PreferredLocations = "East US,South Central US,North Europe" /// public string? PreferredLocations { get; set; } + + internal IEnumerable<(string, object)> SqlQueryParameters { get; set; } } } diff --git a/extensions/Worker.Extensions.CosmosDB/src/CosmosDBOutputAttribute.cs b/extensions/Worker.Extensions.CosmosDB/src/CosmosDBOutputAttribute.cs index 90937d8db..38914de91 100644 --- a/extensions/Worker.Extensions.CosmosDB/src/CosmosDBOutputAttribute.cs +++ b/extensions/Worker.Extensions.CosmosDB/src/CosmosDBOutputAttribute.cs @@ -1,7 +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 Microsoft.Azure.Functions.Worker.Extensions.Abstractions; +using Microsoft.Azure.Functions.Worker.Extensions.Abstractions; namespace Microsoft.Azure.Functions.Worker { diff --git a/extensions/Worker.Extensions.CosmosDB/src/CosmosDBTriggerAttribute.cs b/extensions/Worker.Extensions.CosmosDB/src/CosmosDBTriggerAttribute.cs index a9e586bbb..b262810a0 100644 --- a/extensions/Worker.Extensions.CosmosDB/src/CosmosDBTriggerAttribute.cs +++ b/extensions/Worker.Extensions.CosmosDB/src/CosmosDBTriggerAttribute.cs @@ -1,7 +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; +using System; using Microsoft.Azure.Functions.Worker.Extensions.Abstractions; namespace Microsoft.Azure.Functions.Worker @@ -81,7 +81,7 @@ public CosmosDBTriggerAttribute(string databaseName, string containerName) /// Defines a prefix to be used within a Leases container for this Trigger. Useful when sharing the same Lease container among multiple Triggers /// public string? LeaseContainerPrefix { get; set; } - + /// /// Optional. /// Customizes the delay in milliseconds in between polling a partition for new changes on the feed, after all current changes are drained. Default is 5000 (5 seconds). @@ -120,8 +120,8 @@ public CosmosDBTriggerAttribute(string databaseName, string containerName) /// /// Optional. - /// GGets or sets the a date and time to initialize the change feed read operation from. - /// The recommended format is ISO 8601 with the UTC designator. + /// GGets or sets the a date and time to initialize the change feed read operation from. + /// The recommended format is ISO 8601 with the UTC designator. /// For example: "2021-02-16T14:19:29Z" /// public bool? StartFromTime { get; set; } diff --git a/extensions/Worker.Extensions.CosmosDB/src/CosmosExtensionStartup.cs b/extensions/Worker.Extensions.CosmosDB/src/CosmosExtensionStartup.cs new file mode 100644 index 000000000..84a057eb3 --- /dev/null +++ b/extensions/Worker.Extensions.CosmosDB/src/CosmosExtensionStartup.cs @@ -0,0 +1,31 @@ +using System; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Core; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +[assembly: WorkerExtensionStartup(typeof(CosmosExtensionStartup))] + +namespace Microsoft.Azure.Functions.Worker +{ + public class CosmosExtensionStartup : WorkerExtensionStartup + { + public override void Configure(IFunctionsWorkerApplicationBuilder applicationBuilder) + { + if (applicationBuilder == null) + { + throw new ArgumentNullException(nameof(applicationBuilder)); + } + + applicationBuilder.Services.AddAzureClientsCore(); // Adds AzureComponentFactory + applicationBuilder.Services.AddOptions(); + applicationBuilder.Services.AddSingleton, CosmosDBBindingOptionsSetup>(); + + applicationBuilder.Services.Configure((workerOption) => + { + workerOption.InputConverters.RegisterAt(0); + }); + } + } +} diff --git a/extensions/Worker.Extensions.CosmosDB/src/Properties/AssemblyInfo.cs b/extensions/Worker.Extensions.CosmosDB/src/Properties/AssemblyInfo.cs index 42a07ee20..9da7cf897 100644 --- a/extensions/Worker.Extensions.CosmosDB/src/Properties/AssemblyInfo.cs +++ b/extensions/Worker.Extensions.CosmosDB/src/Properties/AssemblyInfo.cs @@ -3,4 +3,5 @@ using Microsoft.Azure.Functions.Worker.Extensions.Abstractions; -[assembly: ExtensionInformation("Microsoft.Azure.WebJobs.Extensions.CosmosDB", "4.0.0-preview2")] +// [assembly: ExtensionInformation("Microsoft.Azure.WebJobs.Extensions.CosmosDB", "4.0.0-preview2")] +[assembly: ExtensionInformation("Microsoft.Azure.WebJobs.Extensions.CosmosDB", "4.0.0-dev638091464643593480", false, true)] diff --git a/extensions/Worker.Extensions.CosmosDB/src/Utilities.cs b/extensions/Worker.Extensions.CosmosDB/src/Utilities.cs new file mode 100644 index 000000000..1e7332db3 --- /dev/null +++ b/extensions/Worker.Extensions.CosmosDB/src/Utilities.cs @@ -0,0 +1,25 @@ +// 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 System.Linq; + +namespace Microsoft.Azure.Functions.Worker +{ + internal class Utilities + { + internal static IReadOnlyList ParsePreferredLocations(string preferredRegions) + { + if (string.IsNullOrEmpty(preferredRegions)) + { + return Enumerable.Empty().ToList(); + } + + return preferredRegions + .Split(',') + .Select((region) => region.Trim()) + .Where((region) => !string.IsNullOrEmpty(region)) + .ToList(); + } + } +} \ No newline at end of file diff --git a/extensions/Worker.Extensions.CosmosDB/src/Worker.Extensions.CosmosDB.csproj b/extensions/Worker.Extensions.CosmosDB/src/Worker.Extensions.CosmosDB.csproj index cd9e697a0..73f302a91 100644 --- a/extensions/Worker.Extensions.CosmosDB/src/Worker.Extensions.CosmosDB.csproj +++ b/extensions/Worker.Extensions.CosmosDB/src/Worker.Extensions.CosmosDB.csproj @@ -15,8 +15,15 @@ + + + + + + + \ No newline at end of file diff --git a/samples/WorkerBindingSamples/Cosmos/CosmosInputBindingFunctions.cs b/samples/WorkerBindingSamples/Cosmos/CosmosInputBindingFunctions.cs new file mode 100644 index 000000000..dc91fe497 --- /dev/null +++ b/samples/WorkerBindingSamples/Cosmos/CosmosInputBindingFunctions.cs @@ -0,0 +1,208 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.Azure.Cosmos; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; + +namespace SampleApp +{ + public class CosmosInputBindingFunctions + { + private readonly ILogger _logger; + + public CosmosInputBindingFunctions(ILogger logger) + { + _logger = logger; + } + + // Note: attribute should not require databaseName and containerName for CosmosClient + [Function(nameof(DocsByUsingCosmosClient))] + public async Task DocsByUsingCosmosClient( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, + [CosmosDBInput("ToDoItems", "Items", Connection = "CosmosDBConnection")] CosmosClient client) + { + _logger.LogInformation("C# HTTP trigger function processed a request."); + + var iterator = client.GetContainer("ToDoItems", "Items") + .GetItemQueryIterator("SELECT * FROM c"); + + while (iterator.HasMoreResults) + { + var documents = await iterator.ReadNextAsync(); + foreach (dynamic d in documents) + { + Console.WriteLine(d.description); + } + } + + return req.CreateResponse(HttpStatusCode.OK); + } + + // Note: attribute should not require containerName for Database + [Function(nameof(DocsByUsingDatabaseClient))] + public async Task DocsByUsingDatabaseClient( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, + [CosmosDBInput("ToDoItems", "Items", Connection = "CosmosDBConnection")] Database database) + { + _logger.LogInformation("C# HTTP trigger function processed a request."); + + var iterator = database.GetContainerQueryIterator("SELECT * FROM c"); + + while (iterator.HasMoreResults) + { + var containers = await iterator.ReadNextAsync(); + foreach (dynamic c in containers) + { + Console.WriteLine(c.id); + } + } + + return req.CreateResponse(HttpStatusCode.OK); + } + + [Function(nameof(DocsByUsingContainerClient))] + public async Task DocsByUsingContainerClient( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, + [CosmosDBInput("ToDoItems", "Items", Connection = "CosmosDBConnection")] Container container) + { + _logger.LogInformation("C# HTTP trigger function processed a request."); + + var iterator = container.GetItemQueryIterator("SELECT * FROM c"); + + while (iterator.HasMoreResults) + { + var documents = await iterator.ReadNextAsync(); + foreach (dynamic d in documents) + { + Console.WriteLine(d.description); + } + } + + return req.CreateResponse(HttpStatusCode.OK); + } + + [Function(nameof(DocByIdFromRouteData))] + public HttpResponseData DocByIdFromRouteData( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "todoitems/{partitionKey}/{id}")] HttpRequestData req, + [CosmosDBInput( + databaseName: "ToDoItems", + containerName: "Items", + Connection = "CosmosDBConnection", + Id = "{id}", + PartitionKey = "{partitionKey}")] ToDoItem toDoItem) + { + _logger.LogInformation("C# HTTP trigger function processed a request."); + + if (toDoItem == null) + { + _logger.LogInformation($"ToDo item not found"); + } + else + { + _logger.LogInformation($"Found ToDo item, Description={toDoItem.Description}"); + } + + return req.CreateResponse(HttpStatusCode.OK); + } + + [Function(nameof(DocByIdFromQueryString))] + public HttpResponseData DocByIdFromQueryString( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, + [CosmosDBInput( + databaseName: "ToDoItems", + containerName: "Items", + Connection = "CosmosDBConnection", + Id = "{Query.id}", + PartitionKey = "{Query.partitionKey}")] ToDoItem toDoItem) + { + _logger.LogInformation("C# HTTP trigger function processed a request."); + + if (toDoItem == null) + { + _logger.LogInformation($"ToDo item not found"); + } + else + { + _logger.LogInformation($"Found ToDo item, Description={toDoItem.Description}"); + } + + return req.CreateResponse(HttpStatusCode.OK); + } + + // Currently not working, unable to resolve {id} from route + [Function(nameof(DocByIdFromRouteDataUsingSqlQuery))] + public HttpResponseData DocByIdFromRouteDataUsingSqlQuery( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "todoitems2/{id}")] HttpRequestData req, + [CosmosDBInput( + databaseName: "ToDoItems", + containerName: "Items", + Connection = "CosmosDBConnection", + SqlQuery = "SELECT * FROM ToDoItems t where t.id = {id}")] + IEnumerable toDoItems) + { + _logger.LogInformation("C# HTTP trigger function processed a request."); + + foreach (ToDoItem toDoItem in toDoItems) + { + _logger.LogInformation(toDoItem.Description); + } + + return req.CreateResponse(HttpStatusCode.OK); + } + + [Function(nameof(DocsBySqlQuery))] + public HttpResponseData DocsBySqlQuery( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, + [CosmosDBInput( + databaseName: "ToDoItems", + containerName: "Items", + Connection = "CosmosDBConnection", + SqlQuery = "SELECT * FROM ToDoItems t WHERE CONTAINS(t.description, 'cat')")] IEnumerable toDoItems) + { + _logger.LogInformation("C# HTTP trigger function processed a request."); + + foreach (ToDoItem toDoItem in toDoItems) + { + _logger.LogInformation(toDoItem.Description); + } + + return req.CreateResponse(HttpStatusCode.OK); + } + + [Function(nameof(DocByIdFromJSON))] + public void DocByIdFromJSON( + [QueueTrigger("todoqueueforlookup")] ToDoItemLookup toDoItemLookup, + [CosmosDBInput( + databaseName: "ToDoItems", + containerName: "Items", + Connection = "CosmosDBConnection", + Id = "{ToDoItemId}", + PartitionKey = "{ToDoItemPartitionKeyValue}")] ToDoItem toDoItem) + { + _logger.LogInformation($"C# Queue trigger function processed Id={toDoItemLookup?.ToDoItemId} Key={toDoItemLookup?.ToDoItemPartitionKeyValue}"); + + if (toDoItem == null) + { + _logger.LogInformation($"ToDo item not found"); + } + else + { + _logger.LogInformation($"Found ToDo item, Description={toDoItem.Description}"); + } + } + + public class ToDoItemLookup + { + public string ToDoItemId { get; set; } + + public string ToDoItemPartitionKeyValue { get; set; } + } + } +} diff --git a/samples/WorkerBindingSamples/Cosmos/CosmosTriggerFunction.cs b/samples/WorkerBindingSamples/Cosmos/CosmosTriggerFunction.cs new file mode 100644 index 000000000..130e30007 --- /dev/null +++ b/samples/WorkerBindingSamples/Cosmos/CosmosTriggerFunction.cs @@ -0,0 +1,37 @@ +// 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 System.Linq; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Logging; + +namespace SampleApp +{ + // We cannot use trigger bindings with reference types because there is no way for the CosmosDB + // SDK to let us know the ID of the document that triggered the function; therefore we cannot create + // a client that is able to pull the triggering document. + // + // TODO: ensure that for Cosmos trigger binding we do NOT use ParameterBindingData + public static class CosmosTriggerFunction + { + [Function(nameof(CosmosTriggerFunction))] + public static void Run([CosmosDBTrigger( + databaseName: "ToDoItems", + containerName:"TriggerItems", + Connection = "CosmosDBConnection", + CreateLeaseContainerIfNotExists = true)] IReadOnlyList todoItems, + FunctionContext context) + { + var logger = context.GetLogger(nameof(CosmosTriggerFunction)); + + if (todoItems is not null && todoItems.Any()) + { + foreach (var doc in todoItems) + { + logger.LogInformation("ToDoItem: {desc}", doc.Description); + } + } + } + } +} diff --git a/samples/WorkerBindingSamples/Cosmos/ToDoItem.cs b/samples/WorkerBindingSamples/Cosmos/ToDoItem.cs new file mode 100644 index 000000000..0aa36253f --- /dev/null +++ b/samples/WorkerBindingSamples/Cosmos/ToDoItem.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace SampleApp +{ + public class ToDoItem + { + public string Id { get; set; } + public string Description { get; set; } + } +} diff --git a/samples/WorkerBindingSamples/Program.cs b/samples/WorkerBindingSamples/Program.cs new file mode 100644 index 000000000..fc6aa9056 --- /dev/null +++ b/samples/WorkerBindingSamples/Program.cs @@ -0,0 +1,26 @@ +// 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.Diagnostics; +using System.Threading; +using Microsoft.Azure.Functions.Worker.Converters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace SampleApp +{ + public class Program + { + public static void Main() + { + Console.WriteLine($"Azure Functions .NET Worker (PID: { Environment.ProcessId }) initialized in debug mode. Waiting for debugger to attach..."); + + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .Build(); + + host.Run(); + } + } +} diff --git a/samples/WorkerBindingSamples/WorkerBindingSamples.csproj b/samples/WorkerBindingSamples/WorkerBindingSamples.csproj new file mode 100644 index 000000000..418be19d6 --- /dev/null +++ b/samples/WorkerBindingSamples/WorkerBindingSamples.csproj @@ -0,0 +1,38 @@ + + + false + net6.0 + preview + v4 + Exe + <_FunctionsSkipCleanOutput>true + true + ..\..\key.snk + + + DEBUG;TRACE + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + \ No newline at end of file diff --git a/samples/WorkerBindingSamples/host.json b/samples/WorkerBindingSamples/host.json new file mode 100644 index 000000000..beb2e4020 --- /dev/null +++ b/samples/WorkerBindingSamples/host.json @@ -0,0 +1,11 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + } +} \ No newline at end of file