diff --git a/.editorconfig b/.editorconfig index e2f0c4ea8..ee08af36d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,6 +10,9 @@ dotnet_analyzer_diagnostic.severity = error # Namespace does not match folder structure - Ideally this should be enabled but it seems to have issues with root level files so disabling for now dotnet_diagnostic.IDE0130.severity = none +# CA1805: Do not initialize unnecessarily - It's better to be explicit when initializing vars to ensure correct value is used +dotnet_diagnostic.CA1805.severity = none + # Documentation related errors, remove once they are fixed dotnet_diagnostic.CS1591.severity = none dotnet_diagnostic.CS1573.severity = none diff --git a/Directory.Packages.props b/Directory.Packages.props index ebe10d3e5..1211997da 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,5 +15,6 @@ + \ No newline at end of file diff --git a/README.md b/README.md index 6b1246fbf..fb6166bac 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,10 @@ Azure SQL bindings for Azure Functions are supported for: - [Python functions](#python-functions) - [Input Binding Tutorial](#input-binding-tutorial-2) - [Output Binding Tutorial](#output-binding-tutorial-2) + - [Configuration](#configuration) + - [Trigger Binding Configuration](#trigger-binding-configuration) + - [Sql_Trigger_BatchSize](#sql_trigger_batchsize) + - [Sql_Trigger_PollingIntervalMs](#sql_trigger_pollingintervalms) - [More Samples](#more-samples) - [Input Binding](#input-binding) - [Query String](#query-string) @@ -48,7 +52,7 @@ Azure SQL bindings for Azure Functions are supported for: - [Stored Procedure](#stored-procedure) - [IAsyncEnumerable](#iasyncenumerable) - [Output Binding](#output-binding) - - [ICollector/IAsyncCollector](#icollectortiasynccollectort) + - [ICollector<T>/IAsyncCollector<T>](#icollectortiasynccollectort) - [Array](#array) - [Single Row](#single-row) - [Primary Keys and Identity Columns](#primary-keys-and-identity-columns) @@ -240,7 +244,7 @@ Note: This tutorial requires that a SQL database is setup as shown in [Create a - Open your app that you created in [Create a Function App](#create-a-function-app) in VSCode - Press 'F1' and search for 'Azure Functions: Create Function' - Choose HttpTrigger -> (Provide a function name) -> Company.namespace -> anonymous -- In the file that opens, replace the 'public static async Task< IActionResult > Run' block with the below code. +- In the file that opens, replace the `public static async Task Run` block with the below code. ```csharp public static async Task Run( @@ -288,7 +292,7 @@ Note: This tutorial requires that a SQL database is setup as shown in [Create a - Open your app in VSCode - Press 'F1' and search for 'Azure Functions: Create Function' - Choose HttpTrigger -> (Provide a function name) -> Company.namespace is fine -> anonymous -- In the file that opens, replace the 'public static async Task Run' block with the below code +- In the file that opens, replace the `public static async Task Run` block with the below code ```csharp public static IActionResult Run( @@ -377,7 +381,7 @@ Note: This tutorial requires that a SQL database is setup as shown in [Create a - Open your app that you created in [Create a Function App](#create-a-function-app) in VSCode - Press 'F1' and search for 'Azure Functions: Create Function' - Choose HttpTrigger -> (Provide a function name) -> anonymous -- In the file that opens (index.js), replace the 'module.exports = async function (context, req)' block with the below code. +- In the file that opens (`index.js`), replace the `module.exports = async function (context, req)` block with the below code. ```javascript module.exports = async function (context, req, employee) { @@ -417,7 +421,7 @@ Note: This tutorial requires that a SQL database is setup as shown in [Create a - Open your app in VSCode - Press 'F1' and search for 'Azure Functions: Create Function' - Choose HttpTrigger -> (Provide a function name) -> anonymous -- In the file that opens (index.js), replace the 'module.exports = async function (context, req)' block with the below code. +- In the file that opens (`index.js`), replace the `module.exports = async function (context, req)` block with the below code. ```javascript module.exports = async function (context, req) { @@ -472,7 +476,7 @@ Note: This tutorial requires that a SQL database is setup as shown in [Create a - Open your app that you created in [Create a Function App](#create-a-function-app) in VSCode - Press 'F1' and search for 'Azure Functions: Create Function' - Choose HttpTrigger -> (Provide a function name) -> anonymous -- In the file that opens (__init__.py), replace the 'def main(req: func.HttpRequest) -> func.HttpResponse:' block with the below code. +- In the file that opens (`__init__.py`), replace the `def main(req: func.HttpRequest) -> func.HttpResponse:` block with the below code. ```python def main(req: func.HttpRequest, employee: func.SqlRowList) -> func.HttpResponse: @@ -515,7 +519,7 @@ Note: This tutorial requires that a SQL database is setup as shown in [Create a - Open your app in VSCode - Press 'F1' and search for 'Azure Functions: Create Function' - Choose HttpTrigger -> (Provide a function name) -> anonymous -- In the file that opens (__init__.py), replace the 'def main(req: func.HttpRequest) -> func.HttpResponse:' block with the below code. +- In the file that opens (`__init__.py`), replace the `def main(req: func.HttpRequest) -> func.HttpResponse:` block with the below code. ```python def main(req: func.HttpRequest, employee: func.Out[func.SqlRow]) -> func.HttpResponse: @@ -554,6 +558,20 @@ Note: This tutorial requires that a SQL database is setup as shown in [Create a - Hit 'F5' to run your code. Click the link to upsert the output array values in your SQL table. Your upserted values should launch in the browser. - Congratulations! You have successfully created your first SQL output binding! Checkout [Output Binding](#Output-Binding) for more information on how to use it and explore on your own! +## Configuration + +This section goes over some of the configuration values you can use to customize the SQL bindings. See [How to Use Azure Function App Settings](https://learn.microsoft.com/azure/azure-functions/functions-how-to-use-azure-function-app-settings) to learn more. + +### Trigger Binding Configuration + +#### Sql_Trigger_BatchSize + +This controls the number of changes processed at once before being sent to the triggered function. + +#### Sql_Trigger_PollingIntervalMs + +This controls the delay in milliseconds between processing each batch of changes. + ## More Samples ### Input Binding @@ -567,8 +585,8 @@ The input binding takes four [arguments](https://github.com/Azure/azure-function The following are valid binding types for the result of the query/stored procedure execution: -- **IEnumerable**: Each element is a row of the result represented by `T`, where `T` is a user-defined POCO, or Plain Old C# Object. `T` should follow the structure of a row in the queried table. See the [Query String](#query-string) section for an example of what `T` should look like. -- **IAsyncEnumerable**: Each element is again a row of the result represented by `T`, but the rows are retrieved "lazily". A row of the result is only retrieved when `MoveNextAsync` is called on the enumerator. This is useful in the case that the query can return a very large amount of rows. +- **IEnumerable<T>**: Each element is a row of the result represented by `T`, where `T` is a user-defined POCO, or Plain Old C# Object. `T` should follow the structure of a row in the queried table. See the [Query String](#query-string) section for an example of what `T` should look like. +- **IAsyncEnumerable<T>**: Each element is again a row of the result represented by `T`, but the rows are retrieved "lazily". A row of the result is only retrieved when `MoveNextAsync` is called on the enumerator. This is useful in the case that the query can return a very large amount of rows. - **String**: A JSON string representation of the rows of the result (an example is provided [here](https://github.com/Azure/azure-functions-sql-extension/blob/main/samples/samples-csharp/InputBindingSamples/GetProductsString.cs)). - **SqlCommand**: The SqlCommand is populated with the appropriate query and parameters, but the associated connection is not opened. It is the responsiblity of the user to execute the command and read in the results. This is useful in the case that the user wants more control over how the results are read in. An example is provided [here](https://github.com/Azure/azure-functions-sql-extension/blob/main/samples/samples-csharp/InputBindingSamples/GetProductsSqlCommand.cs). @@ -708,13 +726,13 @@ The output binding takes two [arguments](https://github.com/Azure/azure-function The following are valid binding types for the rows to be upserted into the table: -- **ICollector/IAsyncCollector**: Each element is a row represented by `T`, where `T` is a user-defined POCO, or Plain Old C# Object. `T` should follow the structure of a row in the queried table. See the [Query String](#query-string) for an example of what `T` should look like. +- **ICollector<T>/IAsyncCollector<T>**: Each element is a row represented by `T`, where `T` is a user-defined POCO, or Plain Old C# Object. `T` should follow the structure of a row in the queried table. See the [Query String](#query-string) for an example of what `T` should look like. - **T**: Used when just one row is to be upserted into the table. - **T[]**: Each element is again a row of the result represented by `T`. This output binding type requires manual instantiation of the array in the function. The repo contains examples of each of these binding types [here](https://github.com/Azure/azure-functions-sql-extension/tree/main/samples/samples-csharp/OutputBindingSamples). A few examples are also included below. -#### ICollector/IAsyncCollector +#### ICollector<T>/IAsyncCollector<T> When using an `ICollector`, it is not necessary to instantiate it. The function can add rows to the `ICollector` directly, and its contents are automatically upserted once the function exits. diff --git a/WebJobs.Extensions.Sql.sln b/WebJobs.Extensions.Sql.sln index b0b16e416..7600df817 100644 --- a/WebJobs.Extensions.Sql.sln +++ b/WebJobs.Extensions.Sql.sln @@ -20,6 +20,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.WebJobs.Extensions.Sql.Performance", "performance\Microsoft.Azure.WebJobs.Extensions.Sql.Performance.csproj", "{1A5148B7-F877-4813-852C-F94D6EF795E8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "performance", "performance", "{F0F3562F-9176-4461-98E4-13D38D3DD056}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -38,6 +42,10 @@ Global {A5B55530-71C8-4A9A-92AD-1D33A5E80FC1}.Debug|Any CPU.Build.0 = Debug|Any CPU {A5B55530-71C8-4A9A-92AD-1D33A5E80FC1}.Release|Any CPU.ActiveCfg = Release|Any CPU {A5B55530-71C8-4A9A-92AD-1D33A5E80FC1}.Release|Any CPU.Build.0 = Release|Any CPU + {1A5148B7-F877-4813-852C-F94D6EF795E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A5148B7-F877-4813-852C-F94D6EF795E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A5148B7-F877-4813-852C-F94D6EF795E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A5148B7-F877-4813-852C-F94D6EF795E8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -46,6 +54,7 @@ Global {B2E0EFB9-BEDB-48F7-BD5A-0E1010333475} = {3691FB44-971D-43FD-9B2F-916B8CE689DB} {4453B407-2CA3-4011-BDE5-247FCE5C1974} = {21CDC01C-247B-46B0-A8F5-9D35686C628B} {A5B55530-71C8-4A9A-92AD-1D33A5E80FC1} = {F7E99EB5-47D3-4B50-A6AA-D8D5508A121A} + {1A5148B7-F877-4813-852C-F94D6EF795E8} = {F0F3562F-9176-4461-98E4-13D38D3DD056} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {49902AA5-150F-4567-B562-4AA8549B2CF4} diff --git a/builds/TSAConfig.gdntsa b/builds/TSAConfig.gdntsa index 56186440c..11b8aeb1b 100644 --- a/builds/TSAConfig.gdntsa +++ b/builds/TSAConfig.gdntsa @@ -1,7 +1,7 @@ { "codebaseName": "Sql Bindings", "notificationAliases": [ - "sqltools@service.microsoft.com" + "sqlbindings@microsoft.com" ], "codebaseAdmins": [ "REDMOND\\chlafren", @@ -14,6 +14,7 @@ "tools": [ "BinSkim", "RoslynAnalyzers", - "CredScan" + "CredScan", + "Policheck" ] } \ No newline at end of file diff --git a/builds/azure-pipelines/template-steps-build-test.yml b/builds/azure-pipelines/template-steps-build-test.yml index 8b953ebf0..40b6b9165 100644 --- a/builds/azure-pipelines/template-steps-build-test.yml +++ b/builds/azure-pipelines/template-steps-build-test.yml @@ -12,6 +12,14 @@ steps: inputs: useGlobalJson: true +# Run Policheck early to avoid scanning dependency folders +- task: securedevelopmentteam.vss-secure-development-tools.build-task-policheck.PoliCheck@2 + displayName: 'Run PoliCheck' + inputs: + targetType: F + result: PoliCheck.xml + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) + - script: npm install -g azure-functions-core-tools displayName: 'Install Azure Functions Core Tools' @@ -84,12 +92,13 @@ steps: arguments: 'analyze $(Build.SourcesDirectory)\src\bin\${{ parameters.configuration }}\* --recurse --verbose' condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) +# Don't run for PRs since this currently breaks on runs from forks. We run this daily ourselves anyways. - task: securedevelopmentteam.vss-secure-development-tools.build-task-roslynanalyzers.RoslynAnalyzers@3 inputs: userProvideBuildInfo: 'autoMsBuildInfo' env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) - condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'), ne(variables['Build.Reason'], 'PullRequest')) - task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@2 inputs: @@ -135,7 +144,7 @@ steps: inputs: GdnPublishTsaOnboard: true GdnPublishTsaConfigFile: '$(Build.SourcesDirectory)\builds\TSAConfig.gdntsa' - condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['TSA_UPLOAD'], 'true')) # 5.0 isn't supported on Mac yet - task: UseDotNet@2 @@ -253,3 +262,9 @@ steps: displayName: 'Component Detection' inputs: failOnAlert: true + +- task: securedevelopmentteam.vss-secure-development-tools.build-task-postanalysis.PostAnalysis@2 + displayName: 'Post Analysis' + inputs: + GdnBreakPolicyMinSev: Error + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) diff --git a/performance/Microsoft.Azure.WebJobs.Extensions.Sql.Performance.csproj b/performance/Microsoft.Azure.WebJobs.Extensions.Sql.Performance.csproj new file mode 100644 index 000000000..19f8e9efe --- /dev/null +++ b/performance/Microsoft.Azure.WebJobs.Extensions.Sql.Performance.csproj @@ -0,0 +1,36 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + + + + + + + + <_CSharpCopyItems Include="..\samples\samples-csharp\bin\$(Configuration)\$(TargetFramework)\**\*.*" /> + + + + + + + + + Always + %(RecursiveDir)\%(Filename)%(Extension) + True + + + + \ No newline at end of file diff --git a/performance/README.md b/performance/README.md new file mode 100644 index 000000000..f40ec53dd --- /dev/null +++ b/performance/README.md @@ -0,0 +1,16 @@ +# Running Performance Tests + +## Pre-requisites +The performance tests are based on the IntegrationTestBase class. Follow the instructions to set up the pre-requisites for integration tests [here](../test/README.md#running-integration-tests). + +## Run +The performance tests use BenchmarkDotNet to benchmark performance for input and output bindings. + +Run the tests from the terminal. +``` +cd performance +dotnet run -c Release +``` + +## Results +The test results will be generated in the BenchmarkDotNet.Artifacts folder. diff --git a/performance/SqlBindingBenchmarks.cs b/performance/SqlBindingBenchmarks.cs new file mode 100644 index 000000000..b56563db3 --- /dev/null +++ b/performance/SqlBindingBenchmarks.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using BenchmarkDotNet.Running; + +namespace Microsoft.Azure.WebJobs.Extensions.Sql.Performance +{ + public class SqlBindingPerformance + { + public static void Main() + { + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + } + } +} \ No newline at end of file diff --git a/performance/SqlInputBindingPerformance.cs b/performance/SqlInputBindingPerformance.cs new file mode 100644 index 000000000..d657ca3de --- /dev/null +++ b/performance/SqlInputBindingPerformance.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Azure.WebJobs.Extensions.Sql.Samples.Common; +using Microsoft.Azure.WebJobs.Extensions.Sql.Samples.InputBindingSamples; +using Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Common; +using Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Integration; +using BenchmarkDotNet.Attributes; + +namespace Microsoft.Azure.WebJobs.Extensions.Sql.Performance +{ + public class SqlInputBindingPerformance : IntegrationTestBase + { + [GlobalSetup] + public void GlobalSetup() + { + this.StartFunctionHost(nameof(GetProductsTopN), SupportedLanguages.CSharp); + Product[] products = GetProductsWithSameCost(10000, 100); + this.InsertProducts(products); + } + + [Benchmark] + [Arguments("1")] + [Arguments("10")] + [Arguments("100")] + [Arguments("1000")] + [Arguments("10000")] + public async Task GetProductsTest(string count) + { + return await this.SendInputRequest("getproductstopn", count); + } + + [GlobalCleanup] + public void GlobalCleanup() + { + this.Dispose(); + } + } +} \ No newline at end of file diff --git a/performance/SqlOutputBindingPerformance.cs b/performance/SqlOutputBindingPerformance.cs new file mode 100644 index 000000000..e1317b137 --- /dev/null +++ b/performance/SqlOutputBindingPerformance.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Integration; +using Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Common; +using Microsoft.Azure.WebJobs.Extensions.Sql.Samples.Common; +using Microsoft.Azure.WebJobs.Extensions.Sql.Samples.OutputBindingSamples; +using Newtonsoft.Json; +using BenchmarkDotNet.Attributes; + + +namespace Microsoft.Azure.WebJobs.Extensions.Sql.Performance +{ + public class SqlOutputBindingPerformance : IntegrationTestBase + { + [GlobalSetup] + public void StartAddProductsArrayFunction() + { + this.StartFunctionHost(nameof(AddProductsArray), SupportedLanguages.CSharp); + } + + [Benchmark] + [Arguments(1)] + [Arguments(10)] + [Arguments(100)] + [Arguments(1000)] + public async Task AddProductsArrayTest(int count) + { + Product[] productsToAdd = GetProductsWithSameCost(count, 100); + return await this.SendOutputPostRequest("addproducts-array", JsonConvert.SerializeObject(productsToAdd)); + } + + [IterationCleanup] + public void IterationCleanup() + { + // Delete all rows in Products table after each iteration + this.ExecuteNonQuery("TRUNCATE TABLE Products"); + } + + [GlobalCleanup] + public void GlobalCleanup() + { + // Delete the database + this.Dispose(); + } + } +} \ No newline at end of file diff --git a/performance/packages.lock.json b/performance/packages.lock.json new file mode 100644 index 000000000..f72a67a28 --- /dev/null +++ b/performance/packages.lock.json @@ -0,0 +1,2283 @@ +{ + "version": 2, + "dependencies": { + ".NETCoreApp,Version=v3.1": { + "BenchmarkDotNet": { + "type": "Direct", + "requested": "[0.13.1, )", + "resolved": "0.13.1", + "contentHash": "LWR6kL3MWc4ByzSrqi6nccbO4UT5pySiB5h9L2LSHoqVdHySTbtLYYulz3atWhPyhtIQIMz6kQjvuBjFM03zkA==", + "dependencies": { + "BenchmarkDotNet.Annotations": "0.13.1", + "CommandLineParser": "2.4.3", + "Iced": "1.8.0", + "Microsoft.CodeAnalysis.CSharp": "2.10.0", + "Microsoft.Diagnostics.NETCore.Client": "0.2.61701", + "Microsoft.Diagnostics.Runtime": "1.1.126102", + "Microsoft.Diagnostics.Tracing.TraceEvent": "2.0.61", + "Microsoft.DotNet.PlatformAbstractions": "2.1.0", + "Microsoft.Win32.Registry": "4.5.0", + "Perfolizer": "0.2.1", + "System.Management": "4.5.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Threading.Tasks.Extensions": "4.5.2", + "System.ValueTuple": "4.5.0" + } + }, + "Azure.Core": { + "type": "Transitive", + "resolved": "1.19.0", + "contentHash": "lcDjG635DPE4fU5tqSueVMmzrx0QrIfPuY0+y6evHN5GanQ0GB+/4nuMHMmoNPwEow6OUPkJu4cZQxfHJQXPdA==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "1.0.0", + "System.Buffers": "4.5.1", + "System.Diagnostics.DiagnosticSource": "4.6.0", + "System.Memory": "4.5.4", + "System.Memory.Data": "1.0.2", + "System.Numerics.Vectors": "4.5.0", + "System.Text.Encodings.Web": "4.7.2", + "System.Text.Json": "4.6.0", + "System.Threading.Tasks.Extensions": "4.5.2" + } + }, + "Azure.Identity": { + "type": "Transitive", + "resolved": "1.4.0", + "contentHash": "vvjdoDQb9WQyLkD1Uo5KFbwlW7xIsDMihz3yofskym2SimXswbSXuK7QSR1oHnBLBRMdamnVHLpSKQZhJUDejg==", + "dependencies": { + "Azure.Core": "1.14.0", + "Microsoft.Identity.Client": "4.30.1", + "Microsoft.Identity.Client.Extensions.Msal": "2.18.4", + "System.Memory": "4.5.4", + "System.Security.Cryptography.ProtectedData": "4.5.0", + "System.Text.Json": "4.6.0", + "System.Threading.Tasks.Extensions": "4.5.2" + } + }, + "Azure.Storage.Blobs": { + "type": "Transitive", + "resolved": "12.10.0", + "contentHash": "yaijs9DPfn34C/X4TX+0TAxANEhuKSrFE650gkF9g1pz/nQljv86zOOtDwNwD5UsAY5LyrOiCASGo2dhuIxvdg==", + "dependencies": { + "Azure.Storage.Common": "12.9.0", + "System.Text.Json": "4.6.0" + } + }, + "Azure.Storage.Common": { + "type": "Transitive", + "resolved": "12.9.0", + "contentHash": "GuoigTmzz9HrCGdcdu7LyjD4pDr2XPt72LlWWTDyno+nYrjyuNwpwRFBvK/brxJvQFRHofQcBskf8vOxVxnI8g==", + "dependencies": { + "Azure.Core": "1.19.0" + } + }, + "Azure.Storage.Queues": { + "type": "Transitive", + "resolved": "12.8.0", + "contentHash": "YR60fGZHXfUDffSq0zG7RIk9M0LQsS0FNDyslmuP0YS+sOl93TKQXV1wYSROg64VrJoXfLAqP1jRIaeLEhQ/hw==", + "dependencies": { + "Azure.Storage.Common": "12.9.0", + "System.Memory.Data": "1.0.2", + "System.Text.Json": "4.6.0" + } + }, + "BenchmarkDotNet.Annotations": { + "type": "Transitive", + "resolved": "0.13.1", + "contentHash": "OvHMw/GYfdrrJAM28zOXQ94kdv1s0s92ZrbkH+/79I557ONPEH/urMF8iNKuYYgLsziC4isw233L3GKq6Twe/A==" + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "b5rRL5zeaau1y/5hIbI+6mGw3cwun16YjkHZnV9RRT5UyUIFsgLmNXJ0YnIN9p8Hw7K7AbG1q1UclQVU3DinAQ==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "System.Collections.Specialized": "4.3.0", + "System.ComponentModel": "4.3.0", + "System.ComponentModel.TypeConverter": "4.3.0", + "System.Diagnostics.TraceSource": "4.3.0", + "System.Dynamic.Runtime": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Xml.XmlDocument": "4.3.0" + } + }, + "CommandLineParser": { + "type": "Transitive", + "resolved": "2.4.3", + "contentHash": "U2FC9Y8NyIxxU6MpFFdWWu1xwiqz/61v/Doou7kmVjpeIEMLWyiNNkzNlSE84kyJ0O1LKApuEj5z48Ow0Hi4OQ==" + }, + "Iced": { + "type": "Transitive", + "resolved": "1.8.0", + "contentHash": "KQqoTZg3wf+eqG8ztGqlz9TozC/Dw/jnN82JkIRGZXTg/by0aPiQIMGb+b7hvvkOLnmCuWr3Ghr0mA2I+ASX1A==" + }, + "Microsoft.AspNet.WebApi.Client": { + "type": "Transitive", + "resolved": "5.2.4", + "contentHash": "OdBVC2bQWkf9qDd7Mt07ev4SwIdu6VmLBMTWC0D5cOP/HWSXyv/77otwtXVrAo42duNjvXOjzjP5oOI9m1+DTQ==", + "dependencies": { + "Newtonsoft.Json": "10.0.1", + "Newtonsoft.Json.Bson": "1.0.1" + } + }, + "Microsoft.AspNetCore.Authentication.Abstractions": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "7hfl2DQoATexr0OVw8PwJSNqnu9gsbSkuHkwmHdss5xXCuY2nIfsTjj2NoKeGtp6N94ECioAP78FUfFOMj+TTg==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.1.0", + "Microsoft.Extensions.Logging.Abstractions": "2.1.0", + "Microsoft.Extensions.Options": "2.1.0" + } + }, + "Microsoft.AspNetCore.Authentication.Core": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "NKbmBzPW2zTaZLNKkCIL7LMpr4XfXVOPJ5SNzikTe2PX3juLkupb/5oTF45wiw5srUbU6QD0cY9u3jgYUELwnQ==", + "dependencies": { + "Microsoft.AspNetCore.Authentication.Abstractions": "2.1.0", + "Microsoft.AspNetCore.Http": "2.1.0", + "Microsoft.AspNetCore.Http.Extensions": "2.1.0" + } + }, + "Microsoft.AspNetCore.Authorization": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "QUMtMVY7mQeJWlP8wmmhZf1HEGM/V8prW/XnYeKDpEniNBCRw0a3qktRb9aBU0vR+bpJwWZ0ibcB8QOvZEmDHQ==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "2.1.0", + "Microsoft.Extensions.Options": "2.1.0" + } + }, + "Microsoft.AspNetCore.Authorization.Policy": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "e/wxbmwHza+Y6hmM/xiQdsVX5Xh0cPHFbDTGR3kIK7a+jyBSc8CPAJOA5g0ziikLEp5Cm/Qux+CsWad53QoNOw==", + "dependencies": { + "Microsoft.AspNetCore.Authentication.Abstractions": "2.1.0", + "Microsoft.AspNetCore.Authorization": "2.1.0" + } + }, + "Microsoft.AspNetCore.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "1TQgBfd/NPZLR2o/h6l5Cml2ZCF5hsyV4h9WEwWwAIavrbdTnaNozGGcTOd4AOgQvogMM9UM1ajflm9Cwd0jLQ==", + "dependencies": { + "Microsoft.AspNetCore.Hosting.Server.Abstractions": "2.1.0", + "Microsoft.AspNetCore.Http.Abstractions": "2.1.0", + "Microsoft.Extensions.Hosting.Abstractions": "2.1.0" + } + }, + "Microsoft.AspNetCore.Hosting.Server.Abstractions": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "YTKMi2vHX6P+WHEVpW/DS+eFHnwivCSMklkyamcK1ETtc/4j8H3VR0kgW8XIBqukNxhD8k5wYt22P7PhrWSXjQ==", + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "2.1.0", + "Microsoft.Extensions.Configuration.Abstractions": "2.1.0" + } + }, + "Microsoft.AspNetCore.Http.Abstractions": { + "type": "Transitive", + "resolved": "2.1.1", + "contentHash": "kQUEVOU4loc8CPSb2WoHFTESqwIa8Ik7ysCBfTwzHAd0moWovc9JQLmhDIHlYLjHbyexqZAlkq/FPRUZqokebw==", + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "2.1.1", + "System.Text.Encodings.Web": "4.5.0" + } + }, + "Microsoft.AspNetCore.Http.Extensions": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "M8Gk5qrUu5nFV7yE3SZgATt/5B1a5Qs8ZnXXeO/Pqu68CEiBHJWc10sdGdO5guc3zOFdm7H966mVnpZtEX4vSA==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.1.0", + "Microsoft.Extensions.FileProviders.Abstractions": "2.1.0", + "Microsoft.Net.Http.Headers": "2.1.0", + "System.Buffers": "4.5.0" + } + }, + "Microsoft.AspNetCore.Http.Features": { + "type": "Transitive", + "resolved": "2.1.1", + "contentHash": "VklZ7hWgSvHBcDtwYYkdMdI/adlf7ebxTZ9kdzAhX+gUs5jSHE9mZlTamdgf9miSsxc1QjNazHXTDJdVPZKKTw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "2.1.1" + } + }, + "Microsoft.AspNetCore.JsonPatch": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "JE5LRurYn0rglbY/Nj3sB1a+yGPacyYHsuLRgvZtmjLG73R0zEfSIjGmzwtIym0HDLX0RIym8q+BLH4w1nWdog==", + "dependencies": { + "Microsoft.CSharp": "4.5.0", + "Newtonsoft.Json": "11.0.2" + } + }, + "Microsoft.AspNetCore.Mvc.Abstractions": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "NhocJc6vRjxjM8opxpbjYhdN7WbsW07eT5hZOzv87bPxwEL98Hw+D+JIu9DsPm0ce7Rao1qN1BP7w8GMhRFH0Q==", + "dependencies": { + "Microsoft.AspNetCore.Routing.Abstractions": "2.1.0", + "Microsoft.Net.Http.Headers": "2.1.0" + } + }, + "Microsoft.AspNetCore.Mvc.Core": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "AtNtFLtFgZglupwiRK/9ksFg1xAXyZ1otmKtsNSFn9lIwHCQd1xZHIph7GTZiXVWn51jmauIUTUMSWdpaJ+f+A==", + "dependencies": { + "Microsoft.AspNetCore.Authentication.Core": "2.1.0", + "Microsoft.AspNetCore.Authorization.Policy": "2.1.0", + "Microsoft.AspNetCore.Hosting.Abstractions": "2.1.0", + "Microsoft.AspNetCore.Http": "2.1.0", + "Microsoft.AspNetCore.Http.Extensions": "2.1.0", + "Microsoft.AspNetCore.Mvc.Abstractions": "2.1.0", + "Microsoft.AspNetCore.ResponseCaching.Abstractions": "2.1.0", + "Microsoft.AspNetCore.Routing": "2.1.0", + "Microsoft.Extensions.DependencyInjection": "2.1.0", + "Microsoft.Extensions.DependencyModel": "2.1.0", + "Microsoft.Extensions.FileProviders.Abstractions": "2.1.0", + "Microsoft.Extensions.Logging.Abstractions": "2.1.0", + "System.Diagnostics.DiagnosticSource": "4.5.0", + "System.Threading.Tasks.Extensions": "4.5.0" + } + }, + "Microsoft.AspNetCore.Mvc.Formatters.Json": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "Xkbx6LWehUL44rx0gcry+qY013m5LbAjqWfdeisdiSPx2bU/q4EdteRY+zDmO8vT3jKbWcAuvTVUf6AcPPQpTQ==", + "dependencies": { + "Microsoft.AspNetCore.JsonPatch": "2.1.0", + "Microsoft.AspNetCore.Mvc.Core": "2.1.0" + } + }, + "Microsoft.AspNetCore.Mvc.WebApiCompatShim": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "pYsNGveHyMCHQ+xpUIsTHtFFv7Xm+q2pmL3UmL6QujO5ICu/bcnSlwu9FEQhXYQ+cDxfO2VShdM/OrkWzNFGFw==", + "dependencies": { + "Microsoft.AspNet.WebApi.Client": "5.2.4", + "Microsoft.AspNetCore.Mvc.Core": "2.1.0", + "Microsoft.AspNetCore.Mvc.Formatters.Json": "2.1.0", + "Microsoft.AspNetCore.WebUtilities": "2.1.0" + } + }, + "Microsoft.AspNetCore.ResponseCaching.Abstractions": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "Ht/KGFWYqcUDDi+VMPkQNzY7wQ0I2SdqXMEPl6AsOW8hmO3ZS4jIPck6HGxIdlk7ftL9YITJub0cxBmnuq+6zQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "2.1.0" + } + }, + "Microsoft.AspNetCore.Routing": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "eRdsCvtUlLsh0O2Q8JfcpTUhv0m5VCYkgjZTCdniGAq7F31B3gNrBTn9VMqz14m+ZxPUzNqudfDFVTAQlrI/5Q==", + "dependencies": { + "Microsoft.AspNetCore.Http.Extensions": "2.1.0", + "Microsoft.AspNetCore.Routing.Abstractions": "2.1.0", + "Microsoft.Extensions.Logging.Abstractions": "2.1.0", + "Microsoft.Extensions.ObjectPool": "2.1.0", + "Microsoft.Extensions.Options": "2.1.0" + } + }, + "Microsoft.AspNetCore.Routing.Abstractions": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "LXmnHeb3v+HTfn74M46s+4wLaMkplj1Yl2pRf+2mfDDsQ7PN0+h8AFtgip5jpvBvFHQ/Pei7S+cSVsSTHE67fQ==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.1.0" + } + }, + "Microsoft.AspNetCore.WebUtilities": { + "type": "Transitive", + "resolved": "2.1.1", + "contentHash": "PGKIZt4+412Z/XPoSjvYu/QIbTxcAQuEFNoA1Pw8a9mgmO0ZhNBmfaNyhgXFf7Rq62kP0tT/2WXpxdcQhkFUPA==", + "dependencies": { + "Microsoft.Net.Http.Headers": "2.1.1", + "System.Text.Encodings.Web": "4.5.0" + } + }, + "Microsoft.Azure.Functions.Analyzers": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "8nQq/IlK9BMBchRw3lfChSKaFNjMUOxXcPcDC3rkMd5PeWRm54nz2Owr6fZjPHMYJ36XX/9PGOfjn4jyiRojjw==" + }, + "Microsoft.Azure.WebJobs.Core": { + "type": "Transitive", + "resolved": "3.0.31", + "contentHash": "iStV0MQ9env8R2F+8cbaynMK4TDkU6bpPVLdOzIs83iriHYMF+uWF6WZWI8ZJahWR37puQWc86u1YCsoIZEocA==", + "dependencies": { + "System.ComponentModel.Annotations": "4.4.0", + "System.Diagnostics.TraceSource": "4.3.0" + } + }, + "Microsoft.Azure.WebJobs.Extensions": { + "type": "Transitive", + "resolved": "3.0.6", + "contentHash": "y7RgGsJFHhlD/2SIoQpgyjM1By9sbBpY9YaQCS183z743rErqBGAO3BrJ01z52IIveP2NW/7qLD1zm/bFI2MPg==", + "dependencies": { + "Microsoft.Azure.WebJobs": "3.0.14", + "Microsoft.Azure.WebJobs.Host.Storage": "3.0.14", + "ncrontab.signed": "3.3.0" + } + }, + "Microsoft.Azure.WebJobs.Extensions.Http": { + "type": "Transitive", + "resolved": "3.0.2", + "contentHash": "JvC3fESMMbNkYbpaJ4vkK4Xaw1yZy4HSxxqwoaI3Ls2Y5/qBrHftPy0WJQgmXcGjgE/o/aAmuixdTfrj5OQDJQ==", + "dependencies": { + "Microsoft.AspNet.WebApi.Client": "5.2.4", + "Microsoft.AspNetCore.Http": "2.1.0", + "Microsoft.AspNetCore.Mvc.Formatters.Json": "2.1.0", + "Microsoft.AspNetCore.Mvc.WebApiCompatShim": "2.1.0", + "Microsoft.AspNetCore.Routing": "2.1.0", + "Microsoft.Azure.WebJobs": "3.0.2" + } + }, + "Microsoft.Azure.WebJobs.Extensions.Storage.Blobs": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "7zfLwrm2xqx2+NaYpFk3Jp4x56Rr/RHFyawaDbdlGmfK+8UDoCZMkwk65j0eg+bVtxNaorOojK5PfX39kUo9dQ==", + "dependencies": { + "Azure.Storage.Blobs": "12.10.0", + "Azure.Storage.Queues": "12.8.0", + "Microsoft.Azure.WebJobs": "3.0.30", + "Microsoft.Extensions.Azure": "1.1.1" + } + }, + "Microsoft.Azure.WebJobs.Extensions.Storage.Queues": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "TvpZFiQ13W926FxpZxhbP2v1yJyy3tO1chBcJhvJmGE45Rox3sOb9ED36SJVMGEUVS2N/CiJuibrcb2HfPsKZg==", + "dependencies": { + "Azure.Storage.Queues": "12.8.0", + "Microsoft.Azure.WebJobs": "3.0.30", + "Microsoft.Extensions.Azure": "1.1.1" + } + }, + "Microsoft.Azure.WebJobs.Host.Storage": { + "type": "Transitive", + "resolved": "3.0.14", + "contentHash": "1M9VzF4/skqaFezaNI6QUo5ftI1ifGAc6TQfbDdi94lUkUcnSnlBq/uBzx1P73SQgY9WzIE0KZn2+DOx6CL9Sg==", + "dependencies": { + "Microsoft.Azure.WebJobs": "3.0.14", + "WindowsAzure.Storage": "9.3.1" + } + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "K63Y4hORbBcKLWH5wnKgzyn7TOfYzevIEwIedQHBIkmkEBA9SCqgvom+XTuE+fAFGvINGkhFItaZ2dvMGdT5iw==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "2.6.1", + "contentHash": "VsT6gg2SPeToP8SK7PEcsH6Ftryb7aOqnXh9xg11zBeov05+63gP3k/TvrR+v85XIa8Nn0y3+qNl4M+qzNLBfw==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "2.10.0", + "contentHash": "w57ebW3QIRFPoFFX6GCa6eF2FmuHYaWEJ/sMMHq+PBnHB51dEzLIoAQft1Byqe5nrSo4UUV6v4tad8fkTrKl8w==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "2.6.1", + "System.AppContext": "4.3.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Collections.Immutable": "1.5.0", + "System.Console": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.FileVersionInfo": "4.3.0", + "System.Diagnostics.StackTrace": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Dynamic.Runtime": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Metadata": "1.6.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.CodePages": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Tasks.Extensions": "4.3.0", + "System.Threading.Tasks.Parallel": "4.3.0", + "System.Threading.Thread": "4.3.0", + "System.ValueTuple": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0", + "System.Xml.XPath.XDocument": "4.3.0", + "System.Xml.XmlDocument": "4.3.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "2.10.0", + "contentHash": "bTr6j4V7G4ZPhRDUdowdtbEvXsQA4w1TYfOtXiYdv8TF7STl9ShOKtlSVzAusmeEWsZksJm9D1VSxt6XIyNB0w==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[2.10.0]" + } + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "17.0.0", + "contentHash": "+B+09FPYBtf+cXfZOPIgpnP5mzLq5QdlBo+JEFy9CdqBaWHWE/YMY0Mos9uDsZhcgFegJm9GigAgMyqBZyfq+Q==" + }, + "Microsoft.CSharp": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "kaj6Wb4qoMuH3HySFJhxwQfe8R/sJsNJnANrvv8WdFPMoNbKY5htfNscv+LHCu5ipz+49m2e+WQXpLXr9XYemQ==" + }, + "Microsoft.Data.SqlClient.SNI.runtime": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "n1sNyjJgu2pYWKgw3ZPikw3NiRvG4kt7Ya5MK8u77Rgj/1bTFqO/eDF4k5W9H5GXplMZCpKkNbp5kNBICgSB0w==" + }, + "Microsoft.Diagnostics.NETCore.Client": { + "type": "Transitive", + "resolved": "0.2.61701", + "contentHash": "/whUqXLkTiUvG+vfSFd77DHHsLZW2HztZt+ACOpuvGyLKoGGN86M8cR1aYfRW6fxXF3SVGMKMswcL485SQEDuQ==" + }, + "Microsoft.Diagnostics.Runtime": { + "type": "Transitive", + "resolved": "1.1.126102", + "contentHash": "2lyoyld8bd/zSq5HJPkyXVtsSdfS30qr75V96S4nEJ/nUiUp0WfGjxnTcZXBLCqzwE0DLUR0lUcNpMp0gEtuzA==" + }, + "Microsoft.Diagnostics.Tracing.TraceEvent": { + "type": "Transitive", + "resolved": "2.0.61", + "contentHash": "czZJRJZEZbGyBauIXYfWIfVV6Nx88L55RARKmEb7ja+nmb1yI+LiROgnD1N0Fyh/RnzvUUD/J0YYMkAEBT1Z6w==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "4.5.2" + } + }, + "Microsoft.DotNet.PlatformAbstractions": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "9KPDwvb/hLEVXYruVHVZ8BkebC8j17DmPb56LnqRF74HqSPLjCkrlFUjOtFpQPA2DeADBRTI/e69aCfRBfrhxw==", + "dependencies": { + "System.AppContext": "4.1.0", + "System.Collections": "4.0.11", + "System.IO": "4.1.0", + "System.IO.FileSystem": "4.0.1", + "System.Reflection.TypeExtensions": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.InteropServices": "4.1.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.0.0" + } + }, + "Microsoft.Extensions.Azure": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "3BruEhX5qrQ7wSX/2qw6rQJNBuXgXg3gHZyN/64eVpZRPjkgE4+OhrRIpWbNqw+XPg9pGzzjfwdri9v4eOdtsw==", + "dependencies": { + "Azure.Core": "1.19.0", + "Azure.Identity": "1.4.0", + "Microsoft.Extensions.Configuration.Abstractions": "2.1.0", + "Microsoft.Extensions.Configuration.Binder": "2.1.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.0", + "Microsoft.Extensions.Logging.Abstractions": "2.1.0", + "Microsoft.Extensions.Options": "2.1.0" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "2.1.1", + "contentHash": "LjVKO6P2y52c5ZhTLX/w8zc5H4Y3J/LJsgqTBj49TtFq/hAtVNue/WA0F6/7GMY90xhD7K0MDZ4qpOeWXbLvzg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "2.1.1" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "2.1.1", + "contentHash": "VfuZJNa0WUshZ/+8BFZAhwFKiKuu/qOUCFntfdLpHj7vcRnsGHqd3G2Hse78DM+pgozczGM63lGPRLmy+uhUOA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "2.1.1" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "2.1.1", + "contentHash": "fcLCTS03poWE4v9tSNBr3pWn0QwGgAn1vzqHXlXgvqZeOc7LvQNzaWcKRQZTdEc3+YhQKwMsOtm3VKSA2aWQ8w==", + "dependencies": { + "Microsoft.Extensions.Configuration": "2.1.1" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "fZIoU1kxy9zu4KjjabcA79jws6Fk1xmub/VQMrClVqRXZrWt9lYmyjJjw7x0KZtl+Y1hs8qDDaFDrpR1Mso6Wg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "2.1.0" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "xvbjRAIo2Iwxk7vsMg49RwXPOOm5rtvr0frArvlg1uviS60ouVkOLouCNvOv/eRgWYINPbHAU9p//zEjit38Og==", + "dependencies": { + "Microsoft.Extensions.Configuration": "2.1.0", + "Microsoft.Extensions.FileProviders.Physical": "2.1.0" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "9OCdAv7qiRtRlXQnECxW9zINUK8bYPKbNp5x8FQaLZbm/flv7mPvo1muZ1nsKGMZF4uL4Bl6nHw2v1fi3MqQ1Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "2.1.0", + "Microsoft.Extensions.Configuration.FileExtensions": "2.1.0", + "Newtonsoft.Json": "11.0.2" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "gqQviLfuA31PheEGi+XJoZc1bc9H9RsPa9Gq9XuDct7XGWSR9eVXjK5Sg7CSUPhTFHSuxUFY12wcTYLZ4zM1hg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "2.1.1", + "contentHash": "MgYpU5cwZohUMKKg3sbPhvGG+eAZ/59E9UwPwlrUkyXU+PGzqwZg9yyQNjhxuAWmoNoFReoemeCku50prYSGzA==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "nS2XKqi+1A1umnYNLX2Fbm/XnzCxs5i+zXVJ3VC6r9t2z0NZr9FLnJN4VQpKigdcWH/iFTbMuX6M6WQJcTjVIg==", + "dependencies": { + "Microsoft.DotNet.PlatformAbstractions": "2.1.0", + "Newtonsoft.Json": "9.0.1", + "System.Diagnostics.Debug": "4.0.11", + "System.Dynamic.Runtime": "4.0.11", + "System.Linq": "4.1.0" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "itv+7XBu58pxi8mykxx9cUO1OOVYe0jmQIZVSZVp5lOcLxB7sSV2bnHiI1RSu6Nxne/s6+oBla3ON5CCMSmwhQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "2.1.0" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "A9xLomqD4tNFqDfleapx2C14ZcSjCTzn/4Od0W/wBYdlLF2tYDJ204e75HjpWDVTkr03kgdZbM3QZ6ZeDsrBYg==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "2.1.0", + "Microsoft.Extensions.FileSystemGlobbing": "2.1.0" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "JEwwhwbVTEXJu4W4l/FFx7FG9Fh5R8999mZl6qJImjM/LY4DxQsFYzpSkziMdY022n7TQpNUxJlH9bKZc7TqWw==" + }, + "Microsoft.Extensions.Hosting": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "nqOrLtBqpwRT006vdQ2Vp87uiuYztiZcZAndFqH91ZH4SQgr8wImCVQwzUgTxx1DSrpIW765+xrZTZqsoGtvqg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "2.1.0", + "Microsoft.Extensions.DependencyInjection": "2.1.0", + "Microsoft.Extensions.FileProviders.Physical": "2.1.0", + "Microsoft.Extensions.Hosting.Abstractions": "2.1.0", + "Microsoft.Extensions.Logging": "2.1.0" + } + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "BpMaoBxdXr5VD0yk7rYN6R8lAU9X9JbvsPveNdKT+llIn3J5s4sxpWqaSG/NnzTzTLU5eJE5nrecTl7clg/7dQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "2.1.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.0", + "Microsoft.Extensions.FileProviders.Abstractions": "2.1.0", + "Microsoft.Extensions.Logging.Abstractions": "2.1.0" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "2.1.1", + "contentHash": "hh+mkOAQDTp6XH80xJt3+wwYVzkbwYQl9XZRCz4Um0JjP/o7N9vHM3rZ6wwwtr+BBe/L6iBO2sz0px6OWBzqZQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Binder": "2.1.1", + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.1", + "Microsoft.Extensions.Logging.Abstractions": "2.1.1", + "Microsoft.Extensions.Options": "2.1.1" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "2.1.1", + "contentHash": "XRzK7ZF+O6FzdfWrlFTi1Rgj2080ZDsd46vzOjadHUB0Cz5kOvDG8vI7caa5YFrsHQpcfn0DxtjS4E46N4FZsA==" + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "nMAcTACzW37zc3f7n5fIYsRDXtjjQA2U/kiE4xmuSLn7coCIeDfFTpUhJ+wG/3vwb5f1lFWNpyXGyQdlUCIXUw==", + "dependencies": { + "Microsoft.Extensions.Logging": "2.1.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "2.1.0" + } + }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "2.1.1", + "contentHash": "SErON45qh4ogDp6lr6UvVmFYW0FERihW+IQ+2JyFv1PUyWktcJytFaWH5zarufJvZwhci7Rf1IyGXr9pVEadTw==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "2.1.1", + "contentHash": "V7lXCU78lAbzaulCGFKojcCyG8RTJicEbiBkPJjFqiqXwndEBBIehdXRMWEVU3UtzQ1yDvphiWUL9th6/4gJ7w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.1", + "Microsoft.Extensions.Primitives": "2.1.1" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "w/MP147fSqlIcCymaNpLbjdJsFVkSJM9Sz+jbWMr1gKMDVxoOS8AuFjJkVyKU/eydYxHIR/K1Hn3wisJBW5gSg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "2.1.0", + "Microsoft.Extensions.Configuration.Binder": "2.1.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.0", + "Microsoft.Extensions.Options": "2.1.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "2.1.1", + "contentHash": "scJ1GZNIxMmjpENh0UZ8XCQ6vzr/LzeF9WvEA51Ix2OQGAs9WPgPu8ABVUdvpKPLuor/t05gm6menJK3PwqOXg==", + "dependencies": { + "System.Memory": "4.5.1", + "System.Runtime.CompilerServices.Unsafe": "4.5.1" + } + }, + "Microsoft.Identity.Client": { + "type": "Transitive", + "resolved": "4.30.1", + "contentHash": "xk8tJeGfB2yD3+d7a0DXyV7/HYyEG10IofUHYHoPYKmDbroi/j9t1BqSHgbq1nARDjg7m8Ki6e21AyNU7e/R4Q==" + }, + "Microsoft.Identity.Client.Extensions.Msal": { + "type": "Transitive", + "resolved": "2.18.4", + "contentHash": "HpG4oLwhQsy0ce7OWq9iDdLtJKOvKRStIKoSEOeBMKuohfuOWNDyhg8fMAJkpG/kFeoe4J329fiMHcJmmB+FPw==", + "dependencies": { + "Microsoft.Identity.Client": "4.30.0", + "System.Security.Cryptography.ProtectedData": "4.5.0" + } + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "6.8.0", + "contentHash": "+7JIww64PkMt7NWFxoe4Y/joeF7TAtA/fQ0b2GFGcagzB59sKkTt/sMZWR6aSZht5YC7SdHi3W6yM1yylRGJCQ==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "6.8.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "6.8.0", + "contentHash": "Rfh/p4MaN4gkmhPxwbu8IjrmoDncGfHHPh1sTnc0AcM/Oc39/fzC9doKNWvUAjzFb8LqA6lgZyblTrIsX/wDXg==" + }, + "Microsoft.IdentityModel.Protocols": { + "type": "Transitive", + "resolved": "6.8.0", + "contentHash": "OJZx5nPdiH+MEkwCkbJrTAUiO/YzLe0VSswNlDxJsJD9bhOIdXHufh650pfm59YH1DNevp3/bXzukKrG57gA1w==", + "dependencies": { + "Microsoft.IdentityModel.Logging": "6.8.0", + "Microsoft.IdentityModel.Tokens": "6.8.0" + } + }, + "Microsoft.IdentityModel.Protocols.OpenIdConnect": { + "type": "Transitive", + "resolved": "6.8.0", + "contentHash": "X/PiV5l3nYYsodtrNMrNQIVlDmHpjQQ5w48E+o/D5H4es2+4niEyQf3l03chvZGWNzBRhfSstaXr25/Ye4AeYw==", + "dependencies": { + "Microsoft.IdentityModel.Protocols": "6.8.0", + "System.IdentityModel.Tokens.Jwt": "6.8.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "6.8.0", + "contentHash": "gTqzsGcmD13HgtNePPcuVHZ/NXWmyV+InJgalW/FhWpII1D7V1k0obIseGlWMeA4G+tZfeGMfXr0klnWbMR/mQ==", + "dependencies": { + "Microsoft.CSharp": "4.5.0", + "Microsoft.IdentityModel.Logging": "6.8.0", + "System.Security.Cryptography.Cng": "4.5.0" + } + }, + "Microsoft.Net.Http.Headers": { + "type": "Transitive", + "resolved": "2.1.1", + "contentHash": "lPNIphl8b2EuhOE9dMH6EZDmu7pS882O+HMi5BJNsigxHaWlBrYxZHFZgE18cyaPp6SSZcTkKkuzfjV/RRQKlA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "2.1.1", + "System.Buffers": "4.5.0" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "3.1.0", + "contentHash": "z7aeg8oHln2CuNulfhiLYxCVMPEwBl3rzicjvIX+4sUuCwvXw5oXQEtbiU2c0z4qYL5L3Kmx0mMA/+t/SbY67w==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "17.0.0", + "contentHash": "WMugCdPkA8U/BsSRc+3RN+DXcaYSDvp/s0MofVld08iF1O5fek4iKecygk6NruNf1rgJsv4LK71mrwbyeqhzHA==", + "dependencies": { + "NuGet.Frameworks": "5.0.0", + "System.Reflection.Metadata": "1.6.0" + } + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "17.0.0", + "contentHash": "xkKFzm0hylHF0SlDj78ACYMJC/i8fQ3i16sDDNYoKnjTsstGSQfuSBJ+QT4nqRXk/fOiYTh+iY0KIX5N7HTLuQ==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "17.0.0", + "Newtonsoft.Json": "9.0.1" + } + }, + "Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "KSrRMb5vNi0CWSGG1++id2ZOs/1QhRqROt+qgbEAdQuGjGrFcl4AOl4/exGPUYz2wUnU42nvJqon1T3U0kPXLA==", + "dependencies": { + "System.Security.AccessControl": "4.7.0", + "System.Security.Principal.Windows": "4.7.0" + } + }, + "Microsoft.Win32.SystemEvents": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "mtVirZr++rq+XCDITMUdnETD59XoeMxSpLRIII7JRI6Yj0LEDiO1pPn0ktlnIj12Ix8bfvQqQDMMIF9wC98oCA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "3.1.0" + } + }, + "ncrontab.signed": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "w+fVX+uCk3C0nR7BDjWAmUzDQPNAaBusTPljWehx/2cbBTxuKm81sCTebwRnJtHfS+38xbqF7NiiwPWjRMKiFQ==", + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Globalization": "4.0.11", + "System.IO": "4.1.0", + "System.Net.Primitives": "4.0.11", + "System.Resources.ResourceManager": "4.0.1" + } + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.Win32.Primitives": "4.3.0", + "System.AppContext": "4.3.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Console": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.Compression.ZipFile": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.Net.Http": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Net.Sockets": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Timer": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0" + } + }, + "Newtonsoft.Json.Bson": { + "type": "Transitive", + "resolved": "1.0.1", + "contentHash": "5PYT/IqQ+UK31AmZiSS102R6EsTo+LGTSI8bp7WAUqDKaF4wHXD8U9u4WxTI1vc64tYi++8p3dk3WWNqPFgldw==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "Newtonsoft.Json": "10.0.1" + } + }, + "NuGet.Frameworks": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "c5JVjuVAm4f7E9Vj+v09Z9s2ZsqFDjBpcsyS3M9xRo0bEdm/LVZSzLxxNvfvAwRiiE8nwe1h2G4OwiwlzFKXlA==" + }, + "Perfolizer": { + "type": "Transitive", + "resolved": "0.2.1", + "contentHash": "Dt4aCxCT8NPtWBKA8k+FsN/RezOQ2C6omNGm5o/qmYRiIwlQYF93UgFmeF1ezVNsztTnkg7P5P63AE+uNkLfrw==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" + }, + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" + }, + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", + "dependencies": { + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" + } + }, + "runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", + "dependencies": { + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" + }, + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" + }, + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" + }, + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" + }, + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" + }, + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" + }, + "System.AppContext": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "gqpR1EeXOuzNQWL7rOzmtdIz3CaXVjSQCiaGOs2ivjPwynKSJYm39X81fdlp7WuojZs/Z5t1k5ni7HtKQurhjw==" + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "1.5.0", + "contentHash": "EXKiDFsChZW0RjrZ4FYHu9aW6+P4MCgEDCklsVseRfhoO0F+dXeMSsMRAlVXIo06kGJ/zv+2w1a2uc2+kxxSaQ==" + }, + "System.Collections.NonGeneric": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "prtjIEMhGUnQq6RnPEYLpFt8AtLbp9yq2zxOSrY7KJJZrw25Fi97IzBqY7iqssbM61Ek5b8f3MG/sG1N2sN5KA==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Collections.Specialized": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Epx8PoVZR0iuOnJJDzp7pWvdfMMOAvpUo95pC4ScH2mJuXkKA2Y4aR3cG9qt2klHgSons1WFh4kcGW7cSXvrxg==", + "dependencies": { + "System.Collections.NonGeneric": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.ComponentModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VyGn1jGRZVfxnh8EdvDCi71v3bMXrsu8aYJOwoV7SNDLVhiEqwP86pPMyRGsDsxhXAm2b3o9OIqeETfN5qfezw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.ComponentModel.Annotations": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "29K3DQ+IGU7LBaMjTo7SI7T7X/tsMtLvz1p56LJ556Iu0Dw3pKZw5g8yCYCWMRxrOF0Hr0FU0FwW0o42y2sb3A==" + }, + "System.ComponentModel.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "j8GUkCpM8V4d4vhLIIoBLGey2Z5bCkMVNjEZseyAlm4n5arcsJOeI3zkUP+zvZgzsbLTYh4lYeP/ZD/gdIAPrw==", + "dependencies": { + "System.ComponentModel": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.ComponentModel.TypeConverter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "16pQ6P+EdhcXzPiEK4kbA953Fu0MNG2ovxTZU81/qsCd1zPRsKc3uif5NgvllCY598k6bI0KUyKW8fanlfaDQg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Collections.NonGeneric": "4.3.0", + "System.Collections.Specialized": "4.3.0", + "System.ComponentModel": "4.3.0", + "System.ComponentModel.Primitives": "4.3.0", + "System.Globalization": "4.3.0", + "System.Linq": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Configuration.ConfigurationManager": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "/anOTeSZCNNI2zDilogWrZ8pNqCmYbzGNexUnNhjW8k0sHqEZ2nHJBp147jBV3hGYswu5lINpNg1vxR7bnqvVA==", + "dependencies": { + "System.Security.Cryptography.ProtectedData": "4.7.0", + "System.Security.Permissions": "4.7.0" + } + }, + "System.Console": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "oJjw3uFuVDJiJNbCD8HB4a2p3NYLdt1fiT5OGsPLw+WTOuG0KpP4OXelMmmVKpClueMsit6xOlzy4wNKQFiBLg==" + }, + "System.Diagnostics.FileVersionInfo": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "omCF64wzQ3Q2CeIqkD6lmmxeMZtGHUmzgFMPjfVaOsyqpR66p/JaZzManMw1s33osoAb5gqpncsjie67+yUPHQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Reflection.Metadata": "1.4.1", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "System.Diagnostics.StackTrace": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiHg0vgtd35/DM9jvtaC1eKRpWZxr0gcQd643ABG7GnvSlf5pOkY2uyd42mMOJoOmKvnpNj0F4tuoS1pacTwYw==", + "dependencies": { + "System.IO.FileSystem": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Metadata": "1.4.1", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.TraceSource": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VnYp1NxGx8Ww731y2LJ1vpfb/DKVNKEZ8Jsh5SgQTZREL/YpWRArgh9pI8CDLmgHspZmLL697CaLvH85qQpRiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Drawing.Common": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "v+XbyYHaZjDfn0ENmJEV1VYLgGgCTx1gnfOBcppowbpOAriglYgGCvFCPr2EEZyBvXlpxbEsTwkOlInl107ahA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "3.1.0", + "Microsoft.Win32.SystemEvents": "4.7.0" + } + }, + "System.Dynamic.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "SNVi1E/vfWUAs/WYKhE9+qlS6KqK0YVhnlT0HQtr8pMIA8YX3lwy3uPMownDwdYISBdmAF/2holEIldVp85Wag==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Transitive", + "resolved": "6.8.0", + "contentHash": "5tBCjAub2Bhd5qmcd0WhR5s354e4oLYa//kOWrkX+6/7ZbDDJjMTfwLSOiZ/MMpWdE4DWPLOfTLOq/juj9CKzA==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "6.8.0", + "Microsoft.IdentityModel.Tokens": "6.8.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.IO.Compression": "4.3.0" + } + }, + "System.IO.Compression.ZipFile": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", + "dependencies": { + "System.Buffers": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Linq.Expressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Linq": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Management": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "Z6ac0qPGr3yJtwZEX1SRkhwWa0Kf5NJxx7smLboYsGrApQFECNFdqhGy252T4lrZ5Nwzhd9VQiaifndR3bfHdg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "2.0.0", + "Microsoft.Win32.Registry": "4.5.0", + "System.CodeDom": "4.5.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==" + }, + "System.Memory.Data": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "JGkzeqgBsiZwKJZ1IxPNsDFZDhUvuEdX8L8BDC8N3KOj+6zMcNU28CNN59TpZE/VJYy9cP+5M+sbxtWJx3/xtw==", + "dependencies": { + "System.Text.Encodings.Web": "4.7.2", + "System.Text.Json": "4.6.0" + } + }, + "System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.DiagnosticSource": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" + }, + "System.ObjectModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "228FG0jLcIwTVJyz8CLFKueVqQK36ANazUManGaJHkO0icjiIypKW7YLWLIWahyIkdh5M7mV2dJepllLyA1SKg==", + "dependencies": { + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.ILGeneration": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "1.6.0", + "contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ==" + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "4.5.2", + "contentHash": "wprSFgext8cwqymChhrBLu62LMg/1u92bU+VOwyfBimSPVFXtsNqEWC92Pf9ofzJFlk4IHmJA75EDJn1b2goAQ==" + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Runtime.Loader": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DHMaRn8D8YCK2GG2pw+UzNxn/OHVfaWx7OTLBD/hPegHZZgcZh3H6seWegrC4BYwsfuGrywIuT+MQs+rPqRLTQ==", + "dependencies": { + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Numerics": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", + "dependencies": { + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "JECvTt5aFF3WT3gHpfofL2MNNP6v84sxtXxpqhLBCcDRzqsPBmHhQ6shv4DwwN2tRlzsUxtb3G9M3763rbXKDg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "3.1.0", + "System.Security.Principal.Windows": "4.7.0" + } + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.Apple": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Cng": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "WG3r7EyjUe9CMPFSs6bty5doUqT+q9pbI80hlNzo2SkPkZ4VTuZkGWjpp77JB8+uaL4DFPRdBsAY+DX3dBK92A==" + }, + "System.Security.Cryptography.Csp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", + "dependencies": { + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Security.Cryptography.ProtectedData": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "ehYW0m9ptxpGWvE4zgqongBVWpSDU/JCFD4K7krxkQwSz/sFQjEXCUqpvencjy6DYDbn7Ig09R8GFffu8TtneQ==" + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Cng": "4.3.0", + "System.Security.Cryptography.Csp": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Permissions": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "dkOV6YYVBnYRa15/yv004eCGRBVADXw8qRbbNiCn/XpdJSUXkkUeIvdvFHkvnko4CdKMqG8yRHC4ox83LSlMsQ==", + "dependencies": { + "System.Security.AccessControl": "4.7.0", + "System.Windows.Extensions": "4.7.0" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "ojD0PX0XhneCsUbAZVKdb7h/70vyYMDYs85lwEI+LngEONe/17A0cFaRFqZU+sOEidcVswYWikYOQ9PPfjlbtQ==" + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "aeu4FlaUTemuT1qOd1MyU4T516QR4Fy+9yDbwWMPHOHy7U8FD6SgTzdZFO7gHcfAPHtECqInbwklVvUK4RHcNg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "3.1.0" + } + }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "4.7.2", + "contentHash": "iTUgB/WtrZ1sWZs84F2hwyQhiRH6QNjQv2DkwrH+WP6RoFga2Q1m3f9/Q7FG8cck8AdHitQkmkXSY8qylcDmuA==" + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "4.6.0", + "contentHash": "4F8Xe+JIkVoDJ8hDAZ7HqLkjctN/6WItJIzQaifBwClC7wmoLSda/Sv2i6i1kycqDb3hWF4JCVbpAweyOKHEUA==" + }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Threading.Tasks.Dataflow": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "PSIdcgbyNv7FZvZ1I9Mqy6XZOwstYYMdZiXuHvIyc0gDyPjEhrrP9OvTGDHp+LAHp1RNSLjPYssyqox9+Kt9Ug==" + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.2", + "contentHash": "BG/TNxDFv0svAzx8OiMXDlsHfGw623BZ8tCXw4YLhDFDvDhNUEV58jKYMGRnkbJNm7c3JNNJDiN7JBMzxRBR2w==" + }, + "System.Threading.Tasks.Parallel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbjBNZHf/vQCfcdhzx7knsiygoCKgxL8mZOeocXZn5gWhCdzHIq6bYNKWX0LAJCWYP7bds4yBK8p06YkP0oa0g==", + "dependencies": { + "System.Collections.Concurrent": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Thread": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OHmbT+Zz065NKII/ZHcH9XO1dEuLGI1L2k7uYss+9C1jLxTC9kTZZuzUOyXHayRk+dft9CiDf3I/QZ0t8JKyBQ==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Threading.Timer": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.ValueTuple": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==" + }, + "System.Windows.Extensions": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "CeWTdRNfRaSh0pm2gDTJFwVaXfTq6Xwv/sA887iwPTneW7oMtMlpvDIO+U60+3GWTB7Aom6oQwv5VZVUhQRdPQ==", + "dependencies": { + "System.Drawing.Common": "4.7.0" + } + }, + "System.Xml.ReaderWriter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "System.Xml.XDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "System.Xml.XmlDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "lJ8AxvkX7GQxpC6GFCeBj8ThYVyQczx2+f/cWHJU8tjS7YfI6Cv6bon70jVEgs2CiFbmmM8b9j1oZVx0dSI2Ww==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "System.Xml.XPath": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "v1JQ5SETnQusqmS3RwStF7vwQ3L02imIzl++sewmt23VGygix04pEH+FCj1yWb+z4GDzKiljr1W7Wfvrx0YwgA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "System.Xml.XPath.XDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "jw9oHHEIVW53mHY9PgrQa98Xo2IZ0ZjrpdOTmtvk+Rvg4tq7dydmxdNqUvJ5YwjDqhn75mBXWttWjiKhWP53LQ==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0", + "System.Xml.XPath": "4.3.0" + } + }, + "WindowsAzure.Storage": { + "type": "Transitive", + "resolved": "9.3.1", + "contentHash": "NooNF4glP6BO7U4dno/xSfiEVVIv6OFcFfisX24Us2CZa9NQR3TSVEj9eVUlM5rLat5H9CHxk6M/mNSIaq7Vrw==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "Newtonsoft.Json": "10.0.2" + } + }, + "xunit.abstractions": { + "type": "Transitive", + "resolved": "2.0.2", + "contentHash": "vItLB0WkaKg0426RgWq+ZdXH6D+YV/uH28C0weWMOBnVx7I+luHuEYss9hoOngpkiN5kUpLvh9VZRx1H2sk59A==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "0.10.0", + "contentHash": "4/IDFCJfIeg6bix9apmUtIMwvOsiwqdEexeO/R2D4GReIGPLIRODTpId/l4LRSrAJk9lEO3Zx1H0Zx6uohJDNg==" + }, + "xunit.assert": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "Swvkm6iTjZr8TiUj5vMnmfG+2dD4s/BIBgsVOzTxxmoq2ndGsmM2WIL4wuqJ8RhxydWIDOPpIaaytjT2pMTEdg==" + }, + "xunit.core": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "BJ/O/tPEcHUCwQYuwqXoYccTMyw6B5dA6yh7WxWWBhKbjqTsG9RWL0nCQXM5yQYJwUuFzBkiXDPN1BO6UdBB4Q==", + "dependencies": { + "xunit.extensibility.core": "[2.4.0]", + "xunit.extensibility.execution": "[2.4.0]" + } + }, + "xunit.extensibility.core": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "qr/KrR6uukHXD9e/lLQjyCPfMEDuvvhNFDzsYzCF2kKlYKiqcADfUvA9Q68rBtKFtwHFeghjWEuv15KoGD2SfA==", + "dependencies": { + "xunit.abstractions": "2.0.2" + } + }, + "xunit.extensibility.execution": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "252Dzn7i5bMPKtAL15aOP3qJhxKd+57I8ldwIQRJa745JxQuiBu5Da0vtIISVTtc3buRSkBwVnD9iUzsEmCzZA==", + "dependencies": { + "xunit.extensibility.core": "[2.4.0]" + } + }, + "microsoft.azure.webjobs.extensions.sql": { + "type": "Project", + "dependencies": { + "Microsoft.ApplicationInsights": "[2.14.0, )", + "Microsoft.AspNetCore.Http": "[2.1.22, )", + "Microsoft.Azure.WebJobs": "[3.0.31, )", + "Microsoft.Data.SqlClient": "[3.0.1, )", + "Newtonsoft.Json": "[11.0.2, )", + "System.Runtime.Caching": "[4.7.0, )", + "morelinq": "[3.3.2, )" + } + }, + "microsoft.azure.webjobs.extensions.sql.samples": { + "type": "Project", + "dependencies": { + "Microsoft.AspNetCore.Http": "[2.1.22, )", + "Microsoft.Azure.WebJobs.Extensions.Sql": "[99.99.99, )", + "Microsoft.Azure.WebJobs.Extensions.Storage": "[5.0.0, )", + "Microsoft.NET.Sdk.Functions": "[3.1.1, )", + "Newtonsoft.Json": "[11.0.2, )" + } + }, + "microsoft.azure.webjobs.extensions.sql.tests": { + "type": "Project", + "dependencies": { + "Microsoft.AspNetCore.Http": "[2.1.22, )", + "Microsoft.Azure.WebJobs.Extensions.Sql": "[99.99.99, )", + "Microsoft.Azure.WebJobs.Extensions.Sql.Samples": "[1.0.0, )", + "Microsoft.NET.Sdk.Functions": "[3.1.1, )", + "Microsoft.NET.Test.Sdk": "[17.0.0, )", + "Moq": "[4.14.3, )", + "Newtonsoft.Json": "[11.0.2, )", + "xunit": "[2.4.0, )", + "xunit.runner.visualstudio": "[2.4.0, )" + } + }, + "Microsoft.ApplicationInsights": { + "type": "CentralTransitive", + "requested": "[2.14.0, )", + "resolved": "2.14.0", + "contentHash": "j66/uh1Mb5Px/nvMwDWx73Wd9MdO2X+zsDw9JScgm5Siz5r4Cw8sTW+VCrjurFkpFqrNkj+0wbfRHDBD9b+elA==", + "dependencies": { + "System.Diagnostics.DiagnosticSource": "4.6.0", + "System.Memory": "4.5.4" + } + }, + "Microsoft.AspNetCore.Http": { + "type": "CentralTransitive", + "requested": "[2.1.22, )", + "resolved": "2.1.22", + "contentHash": "+Blk++1JWqghbl8+3azQmKhiNZA5wAepL9dY2I6KVmu2Ri07MAcvAVC888qUvO7yd7xgRgZOMfihezKg14O/2A==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.1.1", + "Microsoft.AspNetCore.WebUtilities": "2.1.1", + "Microsoft.Extensions.ObjectPool": "2.1.1", + "Microsoft.Extensions.Options": "2.1.1", + "Microsoft.Net.Http.Headers": "2.1.1" + } + }, + "Microsoft.Azure.WebJobs": { + "type": "CentralTransitive", + "requested": "[3.0.31, )", + "resolved": "3.0.31", + "contentHash": "Jn6E7OgT7LkwVB6lCpjXJcoQIvKrbJT+taVLA4CekEpa21pzZv6nQ2sYRSNzPz5ul3FAcYhmrCQgV7v2iopjgA==", + "dependencies": { + "Microsoft.Azure.WebJobs.Core": "3.0.31", + "Microsoft.Extensions.Configuration": "2.1.1", + "Microsoft.Extensions.Configuration.Abstractions": "2.1.1", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "2.1.0", + "Microsoft.Extensions.Configuration.Json": "2.1.0", + "Microsoft.Extensions.Hosting": "2.1.0", + "Microsoft.Extensions.Logging": "2.1.1", + "Microsoft.Extensions.Logging.Abstractions": "2.1.1", + "Microsoft.Extensions.Logging.Configuration": "2.1.0", + "Newtonsoft.Json": "11.0.2", + "System.Memory.Data": "1.0.1", + "System.Threading.Tasks.Dataflow": "4.8.0" + } + }, + "Microsoft.Azure.WebJobs.Extensions.Storage": { + "type": "CentralTransitive", + "requested": "[5.0.0, )", + "resolved": "5.0.0", + "contentHash": "nu/a8wnkP+dXY6obqRDPoNFcOTElwWQukuAyx5r6bnWi6ybauD2J15dS7sdMb1jHgHQ9LPxWJLLl6W9sYhua/w==", + "dependencies": { + "Microsoft.Azure.WebJobs.Extensions.Storage.Blobs": "5.0.0", + "Microsoft.Azure.WebJobs.Extensions.Storage.Queues": "5.0.0" + } + }, + "Microsoft.Azure.WebJobs.Script.ExtensionsMetadataGenerator": { + "type": "CentralTransitive", + "requested": "[1.2.3, )", + "resolved": "1.2.2", + "contentHash": "vpiNt3JM1pt/WrDIkg7G2DHhIpI4t5I+R9rmXCxIGiby5oPGEolyfiYZdEf2kMMN3SbWzVAbk4Q3jKgFhO9MaQ==", + "dependencies": { + "System.Runtime.Loader": "4.3.0" + } + }, + "Microsoft.Data.SqlClient": { + "type": "CentralTransitive", + "requested": "[3.0.1, )", + "resolved": "3.0.1", + "contentHash": "5Jgcds8yukUeOHvc8S0rGW87rs2uYEM9/YyrYIb/0C+vqzIa2GiqbVPCDVcnApWhs67OSXLTM7lO6jro24v/rA==", + "dependencies": { + "Azure.Identity": "1.3.0", + "Microsoft.Data.SqlClient.SNI.runtime": "3.0.0", + "Microsoft.Identity.Client": "4.22.0", + "Microsoft.IdentityModel.JsonWebTokens": "6.8.0", + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.8.0", + "Microsoft.Win32.Registry": "4.7.0", + "System.Configuration.ConfigurationManager": "4.7.0", + "System.Diagnostics.DiagnosticSource": "4.7.0", + "System.Runtime.Caching": "4.7.0", + "System.Security.Principal.Windows": "4.7.0", + "System.Text.Encoding.CodePages": "4.7.0", + "System.Text.Encodings.Web": "4.7.2" + } + }, + "Microsoft.NET.Sdk.Functions": { + "type": "CentralTransitive", + "requested": "[3.1.1, )", + "resolved": "3.1.1", + "contentHash": "sPPLAjDYroeuIDKwff5B1XrwvGzJm9K9GabXurmfpaXa3M4POy7ngLcG5mm+2pwjTA7e870pIjt1N2DqVQS4yA==", + "dependencies": { + "Microsoft.Azure.Functions.Analyzers": "[1.0.0, 2.0.0)", + "Microsoft.Azure.WebJobs": "[3.0.23, 3.1.0)", + "Microsoft.Azure.WebJobs.Extensions": "3.0.6", + "Microsoft.Azure.WebJobs.Extensions.Http": "[3.0.2, 3.1.0)", + "Microsoft.Azure.WebJobs.Script.ExtensionsMetadataGenerator": "1.2.2", + "Newtonsoft.Json": "11.0.2" + } + }, + "Microsoft.NET.Test.Sdk": { + "type": "CentralTransitive", + "requested": "[17.0.0, )", + "resolved": "17.0.0", + "contentHash": "fJcnMY3jX1MzJvhGvUWauRhU5eQsOaHdwlrcnI3NabBhbi8WLAkMFI8d0YnewA/+b9q/U7vbhp8Xmh1vJ05FYQ==", + "dependencies": { + "Microsoft.CodeCoverage": "17.0.0", + "Microsoft.TestPlatform.TestHost": "17.0.0" + } + }, + "Moq": { + "type": "CentralTransitive", + "requested": "[4.14.3, )", + "resolved": "4.14.3", + "contentHash": "1MB/1YJ9irnhsMXoqJVMUSNZpxpfOJFocLebnd3LMZQ8D8cJE22DxSS+dA7trrHbZf+IWVtbbb0AR+xz/RG/Eg==", + "dependencies": { + "Castle.Core": "4.4.0", + "System.Threading.Tasks.Extensions": "4.5.1" + } + }, + "morelinq": { + "type": "CentralTransitive", + "requested": "[3.3.2, )", + "resolved": "3.3.2", + "contentHash": "MQc8GppZJLmjvcpEdf3EkC6ovsp7gRWt2e5mC7dcIOrgwSc+yjFd3JQ0iRqr3XrUT6rb/phv0IkEmBtbfVA7AQ==" + }, + "Newtonsoft.Json": { + "type": "CentralTransitive", + "requested": "[11.0.2, )", + "resolved": "11.0.2", + "contentHash": "IvJe1pj7JHEsP8B8J8DwlMEx8UInrs/x+9oVY+oCD13jpLu4JbJU2WCIsMRn5C4yW9+DgkaO8uiVE5VHKjpmdQ==" + }, + "System.Runtime.Caching": { + "type": "CentralTransitive", + "requested": "[4.7.0, )", + "resolved": "4.7.0", + "contentHash": "NdvNRjTPxYvIEhXQszT9L9vJhdQoX6AQ0AlhjTU+5NqFQVuacJTfhPVAvtGWNA2OJCqRiR/okBcZgMwI6MqcZg==", + "dependencies": { + "System.Configuration.ConfigurationManager": "4.7.0" + } + }, + "xunit": { + "type": "CentralTransitive", + "requested": "[2.4.0, )", + "resolved": "2.4.0", + "contentHash": "NL00nGsDsyWc1CWxz5FXXjLpW9oFG18WJoTPCyhNv4KGP/e5iLJqAqgM1uaJZyQ6WaTtmWIy4yjYP3RdcaT7Vw==", + "dependencies": { + "xunit.analyzers": "0.10.0", + "xunit.assert": "[2.4.0]", + "xunit.core": "[2.4.0]" + } + }, + "xunit.runner.visualstudio": { + "type": "CentralTransitive", + "requested": "[2.4.0, )", + "resolved": "2.4.0", + "contentHash": "3eq5cGXbEJkqW9nwLuXwtxy9B5gMA8i7HW4rN63AhAvy5UvEcQbZnve23wx/oPrkyg/4CbfNhxkBezS0b1oUdQ==", + "dependencies": { + "Microsoft.NET.Test.Sdk": "15.0.0" + } + } + } + } +} \ No newline at end of file diff --git a/samples/samples-csharp/GlobalSuppressions.cs b/samples/samples-csharp/GlobalSuppressions.cs index 16afef56a..24a32bec9 100644 --- a/samples/samples-csharp/GlobalSuppressions.cs +++ b/samples/samples-csharp/GlobalSuppressions.cs @@ -22,3 +22,4 @@ [assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Unused parameter is required by functions binding", Scope = "member", Target = "~M:Microsoft.Azure.WebJobs.Extensions.Sql.Samples.InputBindingSamples.GetProductNamesView.Run(Microsoft.AspNetCore.Http.HttpRequest,System.Collections.Generic.IEnumerable{Microsoft.Azure.WebJobs.Extensions.Sql.Samples.Common.ProductName})~Microsoft.AspNetCore.Mvc.IActionResult")] [assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Unused parameter is required by functions binding", Scope = "member", Target = "~M:Microsoft.Azure.WebJobs.Extensions.Sql.Samples.InputBindingSamples.GetProductsStoredProcedureFromAppSetting.Run(Microsoft.AspNetCore.Http.HttpRequest,System.Collections.Generic.IEnumerable{Microsoft.Azure.WebJobs.Extensions.Sql.Samples.Common.Product})~Microsoft.AspNetCore.Mvc.IActionResult")] [assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Unused parameter is required by functions binding", Scope = "member", Target = "~M:Microsoft.Azure.WebJobs.Extensions.Sql.Samples.OutputBindingSamples.AddProductsWithIdentityColumnArray.Run(Microsoft.AspNetCore.Http.HttpRequest,Microsoft.Azure.WebJobs.Extensions.Sql.Samples.OutputBindingSamples.ProductWithoutId[]@)~Microsoft.AspNetCore.Mvc.IActionResult")] +[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Unused parameter is required by functions binding", Scope = "member", Target = "~M:Microsoft.Azure.WebJobs.Extensions.Sql.Samples.InputBindingSamples.GetProductsTopN.Run(Microsoft.AspNetCore.Http.HttpRequest,System.Collections.Generic.IEnumerable{Microsoft.Azure.WebJobs.Extensions.Sql.Samples.Common.Product})~Microsoft.AspNetCore.Mvc.IActionResult")] diff --git a/samples/samples-csharp/InputBindingSamples/GetProductsTopN.cs b/samples/samples-csharp/InputBindingSamples/GetProductsTopN.cs new file mode 100644 index 000000000..6f9acdd2b --- /dev/null +++ b/samples/samples-csharp/InputBindingSamples/GetProductsTopN.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Azure.WebJobs.Extensions.Sql.Samples.Common; + +namespace Microsoft.Azure.WebJobs.Extensions.Sql.Samples.InputBindingSamples +{ + public static class GetProductsTopN + { + [FunctionName("GetProductsTopN")] + public static IActionResult Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "getproductstopn/{count}")] + HttpRequest req, + [Sql("SELECT TOP(CAST(@Count AS INT)) * FROM Products", + CommandType = System.Data.CommandType.Text, + Parameters = "@Count={count}", + ConnectionStringSetting = "SqlConnectionString")] + IEnumerable products) + { + return new OkObjectResult(products); + } + } +} diff --git a/samples/samples-csharp/packages.lock.json b/samples/samples-csharp/packages.lock.json index c7f928a9c..a7ba791dc 100644 --- a/samples/samples-csharp/packages.lock.json +++ b/samples/samples-csharp/packages.lock.json @@ -1727,13 +1727,13 @@ "microsoft.azure.webjobs.extensions.sql": { "type": "Project", "dependencies": { - "Microsoft.ApplicationInsights": "2.14.0", - "Microsoft.AspNetCore.Http": "2.1.22", - "Microsoft.Azure.WebJobs": "3.0.31", - "Microsoft.Data.SqlClient": "3.0.1", - "Newtonsoft.Json": "11.0.2", - "System.Runtime.Caching": "4.7.0", - "morelinq": "3.3.2" + "Microsoft.ApplicationInsights": "[2.14.0, )", + "Microsoft.AspNetCore.Http": "[2.1.22, )", + "Microsoft.Azure.WebJobs": "[3.0.31, )", + "Microsoft.Data.SqlClient": "[3.0.1, )", + "Newtonsoft.Json": "[11.0.2, )", + "System.Runtime.Caching": "[4.7.0, )", + "morelinq": "[3.3.2, )" } }, "Microsoft.ApplicationInsights": { diff --git a/src/SqlAsyncCollector.cs b/src/SqlAsyncCollector.cs index b72ef3e0a..238ff89e8 100644 --- a/src/SqlAsyncCollector.cs +++ b/src/SqlAsyncCollector.cs @@ -35,6 +35,11 @@ public PrimaryKey(string name, bool isIdentity) this.Name = name; this.IsIdentity = isIdentity; } + + public override string ToString() + { + return this.Name; + } } /// A user-defined POCO that represents a row of the user's table @@ -147,6 +152,8 @@ public async Task FlushAsync(CancellationToken cancellationToken = default) /// Used to build up the connection private async Task UpsertRowsAsync(IEnumerable rows, SqlAttribute attribute, IConfiguration configuration) { + this._logger.LogDebugWithThreadId("BEGIN UpsertRowsAsync"); + var upsertRowsAsyncSw = Stopwatch.StartNew(); using (SqlConnection connection = SqlBindingUtilities.BuildConnection(attribute.ConnectionStringSetting, configuration)) { await connection.OpenAsync(); @@ -171,7 +178,6 @@ private async Task UpsertRowsAsync(IEnumerable rows, SqlAttribute attribute, AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(10) }; - this._logger.LogInformation($"DB and Table: {connection.Database}.{fullTableName}. Primary keys: [{string.Join(",", tableInfo.PrimaryKeys.Select(pk => pk.Name))}]. SQL Column and Definitions: [{string.Join(",", tableInfo.ColumnDefinitions)}]"); cachedTables.Set(cacheKey, tableInfo, policy); } else @@ -189,6 +195,7 @@ private async Task UpsertRowsAsync(IEnumerable rows, SqlAttribute attribute, } TelemetryInstance.TrackEvent(TelemetryEventName.UpsertStart, props); + this._logger.LogDebugWithThreadId("BEGIN UpsertRowsTransaction"); var transactionSw = Stopwatch.StartNew(); int batchSize = 1000; SqlTransaction transaction = connection.BeginTransaction(); @@ -209,6 +216,8 @@ private async Task UpsertRowsAsync(IEnumerable rows, SqlAttribute attribute, await command.ExecuteNonQueryAsync(); } transaction.Commit(); + transactionSw.Stop(); + upsertRowsAsyncSw.Stop(); var measures = new Dictionary() { { TelemetryMeasureName.BatchCount, batchCount }, @@ -216,7 +225,8 @@ private async Task UpsertRowsAsync(IEnumerable rows, SqlAttribute attribute, { TelemetryMeasureName.CommandDurationMs, commandSw.ElapsedMilliseconds } }; TelemetryInstance.TrackEvent(TelemetryEventName.UpsertEnd, props, measures); - this._logger.LogInformation($"Upserted {rows.Count()} row(s) into database: {connection.Database} and table: {fullTableName}."); + this._logger.LogDebugWithThreadId($"END UpsertRowsTransaction Duration={transactionSw.ElapsedMilliseconds}ms Upserted {rows.Count()} row(s) into database: {connection.Database} and table: {fullTableName}."); + this._logger.LogDebugWithThreadId($"END UpsertRowsAsync Duration={upsertRowsAsyncSw.ElapsedMilliseconds}ms"); } catch (Exception ex) { @@ -300,7 +310,7 @@ private static void GenerateDataQueryForMerge(TableInformation table, IEnumerabl { // SQL Server allows 900 bytes per primary key, so use that as a baseline var combinedPrimaryKey = new StringBuilder(900 * table.PrimaryKeys.Count()); - // Look up primary key of T. Because we're going in the same order of fields every time, + // Look up primary key of T. Because we're going in the same order of properties every time, // we can assume that if two rows with the same primary key are in the list, they will collide foreach (PropertyInfo primaryKey in table.PrimaryKeys) { @@ -344,7 +354,9 @@ private static void GenerateDataQueryForMerge(TableInformation table, IEnumerabl public class TableInformation { - public IEnumerable PrimaryKeys { get; } + private const string ISO_8061_DATETIME_FORMAT = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff"; + + public IEnumerable PrimaryKeys { get; } /// /// All of the columns, along with their data types, for SQL to use to turn JSON into a table @@ -378,7 +390,7 @@ public class TableInformation /// public JsonSerializerSettings JsonSerializerSettings { get; } - public TableInformation(IEnumerable primaryKeys, IDictionary columns, StringComparer comparer, string query, bool hasIdentityColumnPrimaryKeys) + public TableInformation(IEnumerable primaryKeys, IDictionary columns, StringComparer comparer, string query, bool hasIdentityColumnPrimaryKeys) { this.PrimaryKeys = primaryKeys; this.Columns = columns; @@ -386,9 +398,13 @@ public TableInformation(IEnumerable primaryKeys, IDictionary RetrieveTableInformationAsync(SqlConn { Dictionary sqlConnProps = sqlConnection.AsConnectionProps(); TelemetryInstance.TrackEvent(TelemetryEventName.GetTableInfoStart, sqlConnProps); + logger.LogDebugWithThreadId("BEGIN RetrieveTableInformationAsync"); var table = new SqlObject(fullName); // Get case sensitivity from database collation (default to false if any exception occurs) @@ -512,7 +529,9 @@ public static async Task RetrieveTableInformationAsync(SqlConn var caseSensitiveSw = Stopwatch.StartNew(); try { - var cmdCollation = new SqlCommand(GetDatabaseCollationQuery(sqlConnection), sqlConnection); + string getDatabaseCollationQuery = GetDatabaseCollationQuery(sqlConnection); + logger.LogDebugWithThreadId($"BEGIN GetCaseSensitivity Query=\"{getDatabaseCollationQuery}\""); + var cmdCollation = new SqlCommand(getDatabaseCollationQuery, sqlConnection); using (SqlDataReader rdr = await cmdCollation.ExecuteReaderAsync()) { while (await rdr.ReadAsync()) @@ -521,6 +540,7 @@ public static async Task RetrieveTableInformationAsync(SqlConn } caseSensitiveSw.Stop(); TelemetryInstance.TrackDuration(TelemetryEventName.GetCaseSensitivity, caseSensitiveSw.ElapsedMilliseconds, sqlConnProps); + logger.LogDebugWithThreadId($"END GetCaseSensitivity Duration={caseSensitiveSw.ElapsedMilliseconds}ms"); } } catch (Exception ex) @@ -539,7 +559,9 @@ public static async Task RetrieveTableInformationAsync(SqlConn var columnDefinitionsSw = Stopwatch.StartNew(); try { - var cmdColDef = new SqlCommand(GetColumnDefinitionsQuery(table), sqlConnection); + string getColumnDefinitionsQuery = GetColumnDefinitionsQuery(table); + logger.LogDebugWithThreadId($"BEGIN GetColumnDefinitions Query=\"{getColumnDefinitionsQuery}\""); + var cmdColDef = new SqlCommand(getColumnDefinitionsQuery, sqlConnection); using (SqlDataReader rdr = await cmdColDef.ExecuteReaderAsync()) { while (await rdr.ReadAsync()) @@ -549,6 +571,7 @@ public static async Task RetrieveTableInformationAsync(SqlConn } columnDefinitionsSw.Stop(); TelemetryInstance.TrackDuration(TelemetryEventName.GetColumnDefinitions, columnDefinitionsSw.ElapsedMilliseconds, sqlConnProps); + logger.LogDebugWithThreadId($"END GetColumnDefinitions Duration={columnDefinitionsSw.ElapsedMilliseconds}ms"); } } @@ -573,7 +596,9 @@ public static async Task RetrieveTableInformationAsync(SqlConn var primaryKeysSw = Stopwatch.StartNew(); try { - var cmd = new SqlCommand(GetPrimaryKeysQuery(table), sqlConnection); + string getPrimaryKeysQuery = GetPrimaryKeysQuery(table); + logger.LogDebugWithThreadId($"BEGIN GetPrimaryKeys Query=\"{getPrimaryKeysQuery}\""); + var cmd = new SqlCommand(getPrimaryKeysQuery, sqlConnection); using (SqlDataReader rdr = await cmd.ExecuteReaderAsync()) { while (await rdr.ReadAsync()) @@ -583,6 +608,7 @@ public static async Task RetrieveTableInformationAsync(SqlConn } primaryKeysSw.Stop(); TelemetryInstance.TrackDuration(TelemetryEventName.GetPrimaryKeys, primaryKeysSw.ElapsedMilliseconds, sqlConnProps); + logger.LogDebugWithThreadId($"END GetPrimaryKeys Duration={primaryKeysSw.ElapsedMilliseconds}ms"); } } catch (Exception ex) @@ -601,9 +627,9 @@ public static async Task RetrieveTableInformationAsync(SqlConn throw ex; } - // Match SQL Primary Key column names to POCO field/property objects. Ensure none are missing. + // Match SQL Primary Key column names to POCO property objects. Ensure none are missing. StringComparison comparison = caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; - IEnumerable primaryKeyFields = typeof(T).GetMembers().Where(f => primaryKeys.Any(k => string.Equals(k.Name, f.Name, comparison))); + IEnumerable primaryKeyProperties = typeof(T).GetProperties().Where(f => primaryKeys.Any(k => string.Equals(k.Name, f.Name, comparison))); IEnumerable primaryKeysFromObject = columnNames.Where(f => primaryKeys.Any(k => string.Equals(k.Name, f, comparison))); IEnumerable missingPrimaryKeysFromItem = primaryKeys .Where(k => !primaryKeysFromObject.Contains(k.Name, comparer)); @@ -633,7 +659,8 @@ public static async Task RetrieveTableInformationAsync(SqlConn sqlConnProps.Add(TelemetryPropertyName.QueryType, usingInsertQuery ? "insert" : "merge"); sqlConnProps.Add(TelemetryPropertyName.HasIdentityColumn, hasIdentityColumnPrimaryKeys.ToString()); TelemetryInstance.TrackDuration(TelemetryEventName.GetTableInfoEnd, tableInfoSw.ElapsedMilliseconds, sqlConnProps, durations); - return new TableInformation(primaryKeyFields, columnDefinitionsFromSQL, comparer, query, hasIdentityColumnPrimaryKeys); + logger.LogDebugWithThreadId($"END RetrieveTableInformationAsync Duration={tableInfoSw.ElapsedMilliseconds}ms DB and Table: {sqlConnection.Database}.{fullName}. Primary keys: [{string.Join(",", primaryKeyProperties.Select(pk => pk.Name))}]. SQL Column and Definitions: [{string.Join(",", columnDefinitionsFromSQL)}]"); + return new TableInformation(primaryKeyProperties, columnDefinitionsFromSQL, comparer, query, hasIdentityColumnPrimaryKeys); } } diff --git a/src/SqlAsyncEnumerable.cs b/src/SqlAsyncEnumerable.cs index 86519f39a..9b10eab81 100644 --- a/src/SqlAsyncEnumerable.cs +++ b/src/SqlAsyncEnumerable.cs @@ -7,13 +7,12 @@ using System.Threading.Tasks; using Microsoft.Data.SqlClient; using Newtonsoft.Json; - namespace Microsoft.Azure.WebJobs.Extensions.Sql { /// A user-defined POCO that represents a row of the user's table internal class SqlAsyncEnumerable : IAsyncEnumerable { - private readonly SqlConnection _connection; + public SqlConnection Connection { get; private set; } private readonly SqlAttribute _attribute; /// @@ -26,8 +25,9 @@ internal class SqlAsyncEnumerable : IAsyncEnumerable /// public SqlAsyncEnumerable(SqlConnection connection, SqlAttribute attribute) { - this._connection = connection ?? throw new ArgumentNullException(nameof(connection)); + this.Connection = connection ?? throw new ArgumentNullException(nameof(connection)); this._attribute = attribute ?? throw new ArgumentNullException(nameof(attribute)); + this.Connection.Open(); } /// /// Returns the enumerator associated with this enumerable. The enumerator will execute the query specified @@ -38,7 +38,7 @@ public SqlAsyncEnumerable(SqlConnection connection, SqlAttribute attribute) /// The enumerator public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) { - return new SqlAsyncEnumerator(this._connection, this._attribute); + return new SqlAsyncEnumerator(this.Connection, this._attribute); } @@ -47,7 +47,6 @@ private class SqlAsyncEnumerator : IAsyncEnumerator private readonly SqlConnection _connection; private readonly SqlAttribute _attribute; private SqlDataReader _reader; - /// /// Initializes a new instance of the "/> class. /// @@ -77,7 +76,7 @@ public SqlAsyncEnumerator(SqlConnection connection, SqlAttribute attribute) public ValueTask DisposeAsync() { // Doesn't seem like there's an async version of closing the reader/connection - this._reader.Close(); + this._reader?.Close(); this._connection.Close(); return new ValueTask(Task.CompletedTask); } @@ -101,23 +100,24 @@ public ValueTask MoveNextAsync() /// private async Task GetNextRowAsync() { - if (this._reader == null) + // check connection state before trying to access the reader + // if DisposeAsync has already closed it due to the issue described here https://github.com/Azure/azure-functions-sql-extension/issues/350 + if (this._connection.State != System.Data.ConnectionState.Closed) { - using (SqlCommand command = SqlBindingUtilities.BuildCommand(this._attribute, this._connection)) + if (this._reader == null) { - await command.Connection.OpenAsync(); - this._reader = await command.ExecuteReaderAsync(); + using (SqlCommand command = SqlBindingUtilities.BuildCommand(this._attribute, this._connection)) + { + this._reader = await command.ExecuteReaderAsync(); + } + } + if (await this._reader.ReadAsync()) + { + this.Current = JsonConvert.DeserializeObject(this.SerializeRow()); + return true; } } - if (await this._reader.ReadAsync()) - { - this.Current = JsonConvert.DeserializeObject(this.SerializeRow()); - return true; - } - else - { - return false; - } + return false; } /// diff --git a/src/SqlBindingConfigProvider.cs b/src/SqlBindingConfigProvider.cs index d8c6e08ad..f4af37a95 100644 --- a/src/SqlBindingConfigProvider.cs +++ b/src/SqlBindingConfigProvider.cs @@ -55,7 +55,7 @@ public void Initialize(ExtensionConfigContext context) TelemetryInstance.Initialize(this._configuration, logger); #pragma warning disable CS0618 // Fine to use this for our stuff FluentBindingRule inputOutputRule = context.AddBindingRule(); - var converter = new SqlConverter(this._configuration); + var converter = new SqlConverter(this._configuration, logger); inputOutputRule.BindToInput(converter); inputOutputRule.BindToInput(typeof(SqlGenericsConverter), this._configuration, logger); inputOutputRule.BindToCollector(typeof(SqlAsyncCollectorBuilder<>), this._configuration, logger); diff --git a/src/SqlBindingUtilities.cs b/src/SqlBindingUtilities.cs index 8a3df1dc4..6bd29e16f 100644 --- a/src/SqlBindingUtilities.cs +++ b/src/SqlBindingUtilities.cs @@ -136,9 +136,9 @@ public static SqlCommand BuildCommand(SqlAttribute attribute, SqlConnection conn /// /// Used to determine the columns of the table as well as the next SQL row to process /// The built dictionary - public static IReadOnlyDictionary BuildDictionaryFromSqlRow(SqlDataReader reader) + public static IReadOnlyDictionary BuildDictionaryFromSqlRow(SqlDataReader reader) { - return Enumerable.Range(0, reader.FieldCount).ToDictionary(reader.GetName, i => reader.GetValue(i).ToString()); + return Enumerable.Range(0, reader.FieldCount).ToDictionary(reader.GetName, i => reader.GetValue(i)); } /// diff --git a/src/SqlConverters.cs b/src/SqlConverters.cs index d4c18f6c6..628647dfa 100644 --- a/src/SqlConverters.cs +++ b/src/SqlConverters.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Data; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.WebJobs.Extensions.Sql.Telemetry; @@ -21,17 +22,20 @@ internal class SqlConverters internal class SqlConverter : IConverter { private readonly IConfiguration _configuration; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// + /// ILogger used to log information and warnings /// /// Thrown if the configuration is null /// - public SqlConverter(IConfiguration configuration) + public SqlConverter(IConfiguration configuration, ILogger logger) { this._configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + this._logger = logger; TelemetryInstance.TrackCreate(CreateType.SqlConverter); } @@ -46,10 +50,14 @@ public SqlConverter(IConfiguration configuration) public SqlCommand Convert(SqlAttribute attribute) { TelemetryInstance.TrackConvert(ConvertType.SqlCommand); + this._logger.LogDebugWithThreadId("BEGIN Convert (SqlCommand)"); + var sw = Stopwatch.StartNew(); try { - return SqlBindingUtilities.BuildCommand(attribute, SqlBindingUtilities.BuildConnection( + SqlCommand command = SqlBindingUtilities.BuildCommand(attribute, SqlBindingUtilities.BuildConnection( attribute.ConnectionStringSetting, this._configuration)); + this._logger.LogDebugWithThreadId($"END Convert (SqlCommand) Duration={sw.ElapsedMilliseconds}ms"); + return command; } catch (Exception ex) { @@ -97,11 +105,14 @@ public SqlGenericsConverter(IConfiguration configuration, ILogger logger) /// An IEnumerable containing the rows read from the user's database in the form of the user-defined POCO public async Task> ConvertAsync(SqlAttribute attribute, CancellationToken cancellationToken) { - TelemetryInstance.TrackConvert(ConvertType.IEnumerable); + this._logger.LogDebugWithThreadId("BEGIN ConvertAsync (IEnumerable)"); + var sw = Stopwatch.StartNew(); try { - string json = await this.BuildItemFromAttributeAsync(attribute); - return JsonConvert.DeserializeObject>(json); + string json = await this.BuildItemFromAttributeAsync(attribute, ConvertType.IEnumerable); + IEnumerable result = JsonConvert.DeserializeObject>(json); + this._logger.LogDebugWithThreadId($"END ConvertAsync (IEnumerable) Duration={sw.ElapsedMilliseconds}ms"); + return result; } catch (Exception ex) { @@ -128,10 +139,13 @@ public async Task> ConvertAsync(SqlAttribute attribute, Cancellat /// async Task IAsyncConverter.ConvertAsync(SqlAttribute attribute, CancellationToken cancellationToken) { - TelemetryInstance.TrackConvert(ConvertType.Json); + this._logger.LogDebugWithThreadId("BEGIN ConvertAsync (Json)"); + var sw = Stopwatch.StartNew(); try { - return await this.BuildItemFromAttributeAsync(attribute); + string result = await this.BuildItemFromAttributeAsync(attribute, ConvertType.Json); + this._logger.LogDebugWithThreadId($"END ConvertAsync (Json) Duration={sw.ElapsedMilliseconds}ms"); + return result; } catch (Exception ex) { @@ -151,8 +165,11 @@ async Task IAsyncConverter.ConvertAsync(SqlAttribu /// /// The binding attribute that contains the name of the connection string app setting and query. /// + /// + /// The type of conversion being performed by the input binding. + /// /// - public virtual async Task BuildItemFromAttributeAsync(SqlAttribute attribute) + public virtual async Task BuildItemFromAttributeAsync(SqlAttribute attribute, ConvertType type) { using (SqlConnection connection = SqlBindingUtilities.BuildConnection(attribute.ConnectionStringSetting, this._configuration)) // Ideally, we would like to move away from using SqlDataAdapter both here and in the @@ -162,6 +179,8 @@ public virtual async Task BuildItemFromAttributeAsync(SqlAttribute attri { adapter.SelectCommand = command; await connection.OpenAsync(); + Dictionary props = connection.AsConnectionProps(); + TelemetryInstance.TrackConvert(type, props); var dataTable = new DataTable(); adapter.Fill(dataTable); this._logger.LogInformation($"{dataTable.Rows.Count} row(s) queried from database: {connection.Database} using Command: {command.CommandText}"); @@ -172,10 +191,12 @@ public virtual async Task BuildItemFromAttributeAsync(SqlAttribute attri IAsyncEnumerable IConverter>.Convert(SqlAttribute attribute) { - TelemetryInstance.TrackConvert(ConvertType.IAsyncEnumerable); try { - return new SqlAsyncEnumerable(SqlBindingUtilities.BuildConnection(attribute.ConnectionStringSetting, this._configuration), attribute); + var asyncEnumerable = new SqlAsyncEnumerable(SqlBindingUtilities.BuildConnection(attribute.ConnectionStringSetting, this._configuration), attribute); + Dictionary props = asyncEnumerable.Connection.AsConnectionProps(); + TelemetryInstance.TrackConvert(ConvertType.IAsyncEnumerable, props); + return asyncEnumerable; } catch (Exception ex) { @@ -198,10 +219,9 @@ IAsyncEnumerable IConverter>.Convert(SqlAtt /// JArray containing the rows read from the user's database in the form of the user-defined POCO async Task IAsyncConverter.ConvertAsync(SqlAttribute attribute, CancellationToken cancellationToken) { - TelemetryInstance.TrackConvert(ConvertType.JArray); try { - string json = await this.BuildItemFromAttributeAsync(attribute); + string json = await this.BuildItemFromAttributeAsync(attribute, ConvertType.JArray); return JArray.Parse(json); } catch (Exception ex) diff --git a/src/Telemetry/Telemetry.cs b/src/Telemetry/Telemetry.cs index 75853419b..2b3f9b8de 100644 --- a/src/Telemetry/Telemetry.cs +++ b/src/Telemetry/Telemetry.cs @@ -384,6 +384,7 @@ public enum TelemetryMeasureName GetPrimaryKeysDurationMs, InsertGlobalStateTableRowDurationMs, ReleaseLeasesDurationMs, + RetryAttemptNumber, SetLastSyncVersionDurationMs, TransactionDurationMs, UpdateLastSyncVersionDurationMs, @@ -408,6 +409,7 @@ public enum TelemetryErrorName ProcessChanges, PropsNotExistOnTable, ReleaseLeases, + ReleaseLeasesNoRetriesLeft, ReleaseLeasesRollback, RenewLeases, RenewLeasesLoop, diff --git a/src/TriggerBinding/SqlTableChangeMonitor.cs b/src/TriggerBinding/SqlTableChangeMonitor.cs index d0f8241f0..3d161a708 100644 --- a/src/TriggerBinding/SqlTableChangeMonitor.cs +++ b/src/TriggerBinding/SqlTableChangeMonitor.cs @@ -16,6 +16,7 @@ using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using Microsoft.Extensions.Configuration; namespace Microsoft.Azure.WebJobs.Extensions.Sql { @@ -25,16 +26,24 @@ namespace Microsoft.Azure.WebJobs.Extensions.Sql /// POCO class representing the row in the user table internal sealed class SqlTableChangeMonitor : IDisposable { - public const int BatchSize = 10; - public const int PollingIntervalInSeconds = 5; - public const int MaxAttemptCount = 5; - - // Leases are held for approximately (LeaseRenewalIntervalInSeconds * MaxLeaseRenewalCount) seconds. It is + #region Constants + /// + /// The maximum number of times we'll attempt to process a change before giving up + /// + private const int MaxChangeProcessAttemptCount = 5; + /// + /// The maximum number of times that we'll attempt to renew a lease be + /// + /// + /// Leases are held for approximately (LeaseRenewalIntervalInSeconds * MaxLeaseRenewalCount) seconds. It is // required to have at least one of (LeaseIntervalInSeconds / LeaseRenewalIntervalInSeconds) attempts to // renew the lease succeed to prevent it from expiring. - public const int MaxLeaseRenewalCount = 10; - public const int LeaseIntervalInSeconds = 60; - public const int LeaseRenewalIntervalInSeconds = 15; + // + private const int MaxLeaseRenewalCount = 10; + private const int LeaseIntervalInSeconds = 60; + private const int LeaseRenewalIntervalInSeconds = 15; + private const int MaxRetryReleaseLeases = 3; + #endregion Constants private readonly string _connectionString; private readonly int _userTableId; @@ -46,18 +55,30 @@ internal sealed class SqlTableChangeMonitor : IDisposable private readonly IReadOnlyList _rowMatchConditions; private readonly ITriggeredFunctionExecutor _executor; private readonly ILogger _logger; + /// + /// Number of changes to process in each iteration of the loop + /// + private readonly int _batchSize = 10; + /// + /// Delay in ms between processing each batch of changes + /// + private readonly int _pollingIntervalInMs = 5000; - private readonly CancellationTokenSource _cancellationTokenSourceCheckForChanges; - private readonly CancellationTokenSource _cancellationTokenSourceRenewLeases; - private CancellationTokenSource _cancellationTokenSourceExecutor; + private readonly CancellationTokenSource _cancellationTokenSourceCheckForChanges = new CancellationTokenSource(); + private readonly CancellationTokenSource _cancellationTokenSourceRenewLeases = new CancellationTokenSource(); + private CancellationTokenSource _cancellationTokenSourceExecutor = new CancellationTokenSource(); - // The semaphore ensures that mutable class members such as this._rows are accessed by only one thread at a time. - private readonly SemaphoreSlim _rowsLock; + // The semaphore gets used by lease-renewal loop to ensure that '_state' stays set to 'ProcessingChanges' while + // the leases are being renewed. The change-consumption loop requires to wait for the semaphore before modifying + // the value of '_state' back to 'CheckingForChanges'. Since the field '_rows' is only updated if the value of + // '_state' is set to 'CheckingForChanges', this guarantees that '_rows' will stay same while it is being + // iterated over inside the lease-renewal loop. + private readonly SemaphoreSlim _rowsLock = new SemaphoreSlim(1, 1); private readonly IDictionary _telemetryProps; - private IReadOnlyList> _rows; - private int _leaseRenewalCount; + private IReadOnlyList> _rows = new List>(); + private int _leaseRenewalCount = 0; private State _state = State.CheckingForChanges; /// @@ -83,43 +104,28 @@ public SqlTableChangeMonitor( IReadOnlyList primaryKeyColumns, ITriggeredFunctionExecutor executor, ILogger logger, + IConfiguration configuration, IDictionary telemetryProps) { - _ = !string.IsNullOrEmpty(connectionString) ? true : throw new ArgumentNullException(nameof(connectionString)); - _ = !string.IsNullOrEmpty(userTable.FullName) ? true : throw new ArgumentNullException(nameof(userTable)); - _ = !string.IsNullOrEmpty(userFunctionId) ? true : throw new ArgumentNullException(nameof(userFunctionId)); - _ = !string.IsNullOrEmpty(leasesTableName) ? true : throw new ArgumentNullException(nameof(leasesTableName)); - _ = userTableColumns ?? throw new ArgumentNullException(nameof(userTableColumns)); - _ = primaryKeyColumns ?? throw new ArgumentNullException(nameof(primaryKeyColumns)); - _ = executor ?? throw new ArgumentNullException(nameof(executor)); - _ = logger ?? throw new ArgumentNullException(nameof(logger)); - - this._connectionString = connectionString; - this._userTableId = userTableId; - this._userTable = userTable; - this._userFunctionId = userFunctionId; - this._leasesTableName = leasesTableName; - this._userTableColumns = userTableColumns; - this._primaryKeyColumns = primaryKeyColumns; + this._connectionString = !string.IsNullOrEmpty(connectionString) ? connectionString : throw new ArgumentNullException(nameof(connectionString)); + this._userTable = !string.IsNullOrEmpty(userTable?.FullName) ? userTable : throw new ArgumentNullException(nameof(userTable)); + this._userFunctionId = !string.IsNullOrEmpty(userFunctionId) ? userFunctionId : throw new ArgumentNullException(nameof(userFunctionId)); + this._leasesTableName = !string.IsNullOrEmpty(leasesTableName) ? leasesTableName : throw new ArgumentNullException(nameof(leasesTableName)); + this._userTableColumns = userTableColumns ?? throw new ArgumentNullException(nameof(userTableColumns)); + this._primaryKeyColumns = primaryKeyColumns ?? throw new ArgumentNullException(nameof(primaryKeyColumns)); + this._executor = executor ?? throw new ArgumentNullException(nameof(executor)); + this._logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this._userTableId = userTableId; + // Check if there's config settings to override the default batch size/polling interval values + this._batchSize = configuration.GetValue(SqlTriggerConstants.ConfigKey_SqlTrigger_BatchSize) ?? this._batchSize; + this._pollingIntervalInMs = configuration.GetValue(SqlTriggerConstants.ConfigKey_SqlTrigger_PollingInterval) ?? this._pollingIntervalInMs; // Prep search-conditions that will be used besides WHERE clause to match table rows. - this._rowMatchConditions = Enumerable.Range(0, BatchSize) + this._rowMatchConditions = Enumerable.Range(0, this._batchSize) .Select(rowIndex => string.Join(" AND ", this._primaryKeyColumns.Select((col, colIndex) => $"{col.AsBracketQuotedString()} = @{rowIndex}_{colIndex}"))) .ToList(); - this._executor = executor; - this._logger = logger; - - this._cancellationTokenSourceCheckForChanges = new CancellationTokenSource(); - this._cancellationTokenSourceRenewLeases = new CancellationTokenSource(); - this._cancellationTokenSourceExecutor = new CancellationTokenSource(); - - this._telemetryProps = telemetryProps; - - this._rowsLock = new SemaphoreSlim(1); - this._rows = new List>(); - this._leaseRenewalCount = 0; - this._state = State.CheckingForChanges; + this._telemetryProps = telemetryProps ?? new Dictionary(); #pragma warning disable CS4014 // Queue the below tasks and exit. Do not wait for their completion. _ = Task.Run(() => @@ -136,7 +142,7 @@ public void Dispose() } /// - /// Executed once every period. If the state of the change monitor is + /// Executed once every period. If the state of the change monitor is /// , then the method query the change/leases tables for changes on the /// user's table. If any are found, the state of the change monitor is transitioned to /// and the user's function is executed with the found changes. If the @@ -145,7 +151,7 @@ public void Dispose() /// private async Task RunChangeConsumptionLoopAsync() { - this._logger.LogDebugWithThreadId("Starting change consumption loop."); + this._logger.LogInformationWithThreadId($"Starting change consumption loop. BatchSize: {this._batchSize} PollingIntervalMs: {this._pollingIntervalInMs}"); try { @@ -156,6 +162,7 @@ private async Task RunChangeConsumptionLoopAsync() this._logger.LogDebugWithThreadId("BEGIN OpenChangeConsumptionConnection"); await connection.OpenAsync(token); this._logger.LogDebugWithThreadId("END OpenChangeConsumptionConnection"); + // Check for cancellation request only after a cycle of checking and processing of changes completes. while (!token.IsCancellationRequested) { @@ -166,7 +173,8 @@ private async Task RunChangeConsumptionLoopAsync() await this.ProcessTableChangesAsync(connection, token); } this._logger.LogDebugWithThreadId("END CheckingForChanges"); - await Task.Delay(TimeSpan.FromSeconds(PollingIntervalInSeconds), token); + this._logger.LogDebugWithThreadId($"Delaying for {this._pollingIntervalInMs}ms"); + await Task.Delay(TimeSpan.FromMilliseconds(this._pollingIntervalInMs), token); } } } @@ -217,12 +225,13 @@ private async Task GetTableChangesAsync(SqlConnection connection, CancellationTo } this._logger.LogDebugWithThreadId($"END UpdateTablesPreInvocation Duration={setLastSyncVersionDurationMs}ms"); + var rows = new List>(); + // Use the version number to query for new changes. using (SqlCommand getChangesCommand = this.BuildGetChangesCommand(connection, transaction)) { this._logger.LogDebugWithThreadId($"BEGIN GetChanges Query={getChangesCommand.CommandText}"); var commandSw = Stopwatch.StartNew(); - var rows = new List>(); using (SqlDataReader reader = await getChangesCommand.ExecuteReaderAsync(token)) { @@ -232,15 +241,14 @@ private async Task GetTableChangesAsync(SqlConnection connection, CancellationTo } } - this._rows = rows; getChangesDurationMs = commandSw.ElapsedMilliseconds; } - this._logger.LogDebugWithThreadId($"END GetChanges Duration={getChangesDurationMs}ms ChangedRows={this._rows.Count}"); + this._logger.LogDebugWithThreadId($"END GetChanges Duration={getChangesDurationMs}ms ChangedRows={rows.Count}"); // If changes were found, acquire leases on them. - if (this._rows.Count > 0) + if (rows.Count > 0) { - using (SqlCommand acquireLeasesCommand = this.BuildAcquireLeasesCommand(connection, transaction)) + using (SqlCommand acquireLeasesCommand = this.BuildAcquireLeasesCommand(connection, transaction, rows)) { this._logger.LogDebugWithThreadId($"BEGIN AcquireLeases Query={acquireLeasesCommand.CommandText}"); var commandSw = Stopwatch.StartNew(); @@ -252,6 +260,9 @@ private async Task GetTableChangesAsync(SqlConnection connection, CancellationTo transaction.Commit(); + // Set the rows for processing, now since the leases are acquired. + this._rows = rows; + var measures = new Dictionary { [TelemetryMeasureName.SetLastSyncVersionDurationMs] = setLastSyncVersionDurationMs, @@ -284,7 +295,7 @@ private async Task GetTableChangesAsync(SqlConnection connection, CancellationTo { // If there's an exception in any part of the process, we want to clear all of our data in memory and // retry checking for changes again. - this._rows = new List>(); + this._rows = new List>(); this._logger.LogError($"Failed to check for changes in table '{this._userTable.FullName}' due to exception: {e.GetType()}. Exception message: {e.Message}"); TelemetryInstance.TrackException(TelemetryErrorName.GetChanges, e, this._telemetryProps); } @@ -311,7 +322,7 @@ private async Task ProcessTableChangesAsync(SqlConnection connection, Cancellati { this._logger.LogError($"Failed to compose trigger parameter value for table: '{this._userTable.FullName} due to exception: {e.GetType()}. Exception message: {e.Message}"); TelemetryInstance.TrackException(TelemetryErrorName.ProcessChanges, e, this._telemetryProps); - await this.ClearRowsAsync(true); + await this.ClearRowsAsync(); } if (changes != null) @@ -343,7 +354,7 @@ private async Task ProcessTableChangesAsync(SqlConnection connection, Cancellati this._logger.LogError($"Failed to trigger user function for table: '{this._userTable.FullName} due to exception: {result.Exception.GetType()}. Exception message: {result.Exception.Message}"); TelemetryInstance.TrackException(TelemetryErrorName.ProcessChanges, result.Exception, this._telemetryProps, measures); - await this.ClearRowsAsync(true); + await this.ClearRowsAsync(); } } } @@ -367,11 +378,9 @@ private async void RunLeaseRenewalLoopAsync() this._logger.LogDebugWithThreadId("BEGIN OpenLeaseRenewalLoopConnection"); await connection.OpenAsync(token); this._logger.LogDebugWithThreadId("END OpenLeaseRenewalLoopConnection"); + while (!token.IsCancellationRequested) { - this._logger.LogDebugWithThreadId("BEGIN WaitRowsLock - LeaseRenewal"); - await this._rowsLock.WaitAsync(token); - this._logger.LogDebugWithThreadId("END WaitRowsLock - LeaseRenewal"); await this.RenewLeasesAsync(connection, token); await Task.Delay(TimeSpan.FromSeconds(LeaseRenewalIntervalInSeconds), token); } @@ -395,9 +404,13 @@ private async void RunLeaseRenewalLoopAsync() private async Task RenewLeasesAsync(SqlConnection connection, CancellationToken token) { - try + this._logger.LogDebugWithThreadId("BEGIN WaitRowsLock - RenewLeases"); + await this._rowsLock.WaitAsync(token); + this._logger.LogDebugWithThreadId("END WaitRowsLock - RenewLeases"); + + if (this._state == State.ProcessingChanges) { - if (this._state == State.ProcessingChanges) + try { // I don't think I need a transaction for renewing leases. If this worker reads in a row from the // leases table and determines that it corresponds to its batch of changes, but then that row gets @@ -420,19 +433,16 @@ private async Task RenewLeasesAsync(SqlConnection connection, CancellationToken TelemetryInstance.TrackEvent(TelemetryEventName.RenewLeasesEnd, this._telemetryProps, measures); } } - } - catch (Exception e) - { - // This catch block is necessary so that the finally block is executed even in the case of an exception - // (see https://docs.microsoft.com/dotnet/csharp/language-reference/keywords/try-finally, third - // paragraph). If we fail to renew the leases, multiple workers could be processing the same change - // data, but we have functionality in place to deal with this (see design doc). - this._logger.LogError($"Failed to renew leases due to exception: {e.GetType()}. Exception message: {e.Message}"); - TelemetryInstance.TrackException(TelemetryErrorName.RenewLeases, e, this._telemetryProps); - } - finally - { - if (this._state == State.ProcessingChanges) + catch (Exception e) + { + // This catch block is necessary so that the finally block is executed even in the case of an exception + // (see https://docs.microsoft.com/dotnet/csharp/language-reference/keywords/try-finally, third + // paragraph). If we fail to renew the leases, multiple workers could be processing the same change + // data, but we have functionality in place to deal with this (see design doc). + this._logger.LogError($"Failed to renew leases due to exception: {e.GetType()}. Exception message: {e.Message}"); + TelemetryInstance.TrackException(TelemetryErrorName.RenewLeases, e, this._telemetryProps); + } + finally { // Do we want to update this count even in the case of a failure to renew the leases? Probably, // because the count is simply meant to indicate how much time the other thread has spent processing @@ -452,30 +462,27 @@ private async Task RenewLeasesAsync(SqlConnection connection, CancellationToken this._cancellationTokenSourceExecutor = new CancellationTokenSource(); } } - - // Want to always release the lock at the end, even if renewing the leases failed. - this._logger.LogDebugWithThreadId("ReleaseRowLock - RenewLeases"); - this._rowsLock.Release(); } + + // Want to always release the lock at the end, even if renewing the leases failed. + this._logger.LogDebugWithThreadId("ReleaseRowsLock - RenewLeases"); + this._rowsLock.Release(); } /// /// Resets the in-memory state of the change monitor and sets it to start polling for changes again. /// - /// True if ClearRowsAsync should acquire the "_rowsLock" (only true in the case of a failure) - private async Task ClearRowsAsync(bool acquireLock) + private async Task ClearRowsAsync() { - if (acquireLock) - { - this._logger.LogDebugWithThreadId("BEGIN WaitRowsLock - ClearRows"); - await this._rowsLock.WaitAsync(); - this._logger.LogDebugWithThreadId("END WaitRowsLock - ClearRows"); - } + this._logger.LogDebugWithThreadId("BEGIN WaitRowsLock - ClearRows"); + await this._rowsLock.WaitAsync(); + this._logger.LogDebugWithThreadId("END WaitRowsLock - ClearRows"); this._leaseRenewalCount = 0; this._state = State.CheckingForChanges; - this._rows = new List>(); - this._logger.LogDebugWithThreadId("ReleaseRowLock - ClearRows"); + this._rows = new List>(); + + this._logger.LogDebugWithThreadId("ReleaseRowsLock - ClearRows"); this._rowsLock.Release(); } @@ -486,13 +493,10 @@ private async Task ClearRowsAsync(bool acquireLock) private async Task ReleaseLeasesAsync(SqlConnection connection, CancellationToken token) { TelemetryInstance.TrackEvent(TelemetryEventName.ReleaseLeasesStart, this._telemetryProps); - this._logger.LogDebugWithThreadId("BEGIN WaitRowsLock - ReleaseLeases"); - // Don't want to change the "_rows" while another thread is attempting to renew leases on them. - await this._rowsLock.WaitAsync(token); - this._logger.LogDebugWithThreadId("END WaitRowsLock - ReleaseLeases"); long newLastSyncVersion = this.RecomputeLastSyncVersion(); + bool retrySucceeded = false; - try + for (int retryCount = 1; retryCount <= MaxRetryReleaseLeases && !retrySucceeded; retryCount++) { var transactionSw = Stopwatch.StartNew(); long releaseLeasesDurationMs = 0L, updateLastSyncVersionDurationMs = 0L; @@ -528,14 +532,30 @@ private async Task ReleaseLeasesAsync(SqlConnection connection, CancellationToke { [TelemetryMeasureName.ReleaseLeasesDurationMs] = releaseLeasesDurationMs, [TelemetryMeasureName.UpdateLastSyncVersionDurationMs] = updateLastSyncVersionDurationMs, + [TelemetryMeasureName.TransactionDurationMs] = transactionSw.ElapsedMilliseconds, }; TelemetryInstance.TrackEvent(TelemetryEventName.ReleaseLeasesEnd, this._telemetryProps, measures); + retrySucceeded = true; } catch (Exception ex) { - this._logger.LogError($"Failed to execute SQL commands to release leases for table '{this._userTable.FullName}' due to exception: {ex.GetType()}. Exception message: {ex.Message}"); - TelemetryInstance.TrackException(TelemetryErrorName.ReleaseLeases, ex, this._telemetryProps); + if (retryCount < MaxRetryReleaseLeases) + { + this._logger.LogError($"Failed to execute SQL commands to release leases in attempt: {retryCount} for table '{this._userTable.FullName}' due to exception: {ex.GetType()}. Exception message: {ex.Message}"); + + var measures = new Dictionary + { + [TelemetryMeasureName.RetryAttemptNumber] = retryCount, + }; + + TelemetryInstance.TrackException(TelemetryErrorName.ReleaseLeases, ex, this._telemetryProps, measures); + } + else + { + this._logger.LogError($"Failed to release leases for table '{this._userTable.FullName}' after {MaxRetryReleaseLeases} attempts due to exception: {ex.GetType()}. Exception message: {ex.Message}"); + TelemetryInstance.TrackException(TelemetryErrorName.ReleaseLeasesNoRetriesLeft, ex, this._telemetryProps); + } try { @@ -549,20 +569,8 @@ private async Task ReleaseLeasesAsync(SqlConnection connection, CancellationToke } } } - catch (Exception e) - { - // What should we do if releasing the leases fails? We could try to release them again or just wait, - // since eventually the lease time will expire. Then another thread will re-process the same changes - // though, so less than ideal. But for now that's the functionality. - this._logger.LogError($"Failed to release leases for table '{this._userTable.FullName}' due to exception: {e.GetType()}. Exception message: {e.Message}"); - TelemetryInstance.TrackException(TelemetryErrorName.ReleaseLeases, e, this._telemetryProps); - } - finally - { - // Want to do this before releasing the lock in case the renew leases thread wakes up. It will see that - // the state is checking for changes and not renew the (just released) leases. - await this.ClearRowsAsync(false); - } + + await this.ClearRowsAsync(); } /// @@ -571,13 +579,20 @@ private async Task ReleaseLeasesAsync(SqlConnection connection, CancellationToke private long RecomputeLastSyncVersion() { var changeVersionSet = new SortedSet(); - foreach (IReadOnlyDictionary row in this._rows) + foreach (IReadOnlyDictionary row in this._rows) { - string changeVersion = row["SYS_CHANGE_VERSION"]; + string changeVersion = row["SYS_CHANGE_VERSION"].ToString(); changeVersionSet.Add(long.Parse(changeVersion, CultureInfo.InvariantCulture)); } - // If there are more than one version numbers in the set, return the second highest one. Otherwise, return + + // The batch of changes are gotten in ascending order of the version number. + // With this, it is ensured that if there are multiple version numbers in the changeVersionSet, + // all the other rows with version numbers less than the highest should have either been processed or + // have leases acquired on them by another worker. + // Therefore, if there are more than one version numbers in the set, return the second highest one. Otherwise, return // the only version number in the set. + // Also this LastSyncVersion is actually updated in the GlobalState table only after verifying that the changes with + // changeVersion <= newLastSyncVersion have been processed in BuildUpdateTablesPostInvocation query. long lastSyncVersion = changeVersionSet.ElementAt(changeVersionSet.Count > 1 ? changeVersionSet.Count - 2 : 0); this._logger.LogDebugWithThreadId($"RecomputeLastSyncVersion. LastSyncVersion={lastSyncVersion} ChangeVersionSet={string.Join(",", changeVersionSet)}"); return lastSyncVersion; @@ -593,13 +608,13 @@ private IReadOnlyList> ProcessChanges() { this._logger.LogDebugWithThreadId("BEGIN ProcessChanges"); var changes = new List>(); - foreach (IReadOnlyDictionary row in this._rows) + foreach (IReadOnlyDictionary row in this._rows) { SqlChangeOperation operation = GetChangeOperation(row); // If the row has been deleted, there is no longer any data for it in the user table. The best we can do // is populate the row-item with the primary key values of the row. - Dictionary item = operation == SqlChangeOperation.Delete + Dictionary item = operation == SqlChangeOperation.Delete ? this._primaryKeyColumns.ToDictionary(col => col, col => row[col]) : this._userTableColumns.ToDictionary(col => col, col => row[col]); @@ -615,9 +630,9 @@ private IReadOnlyList> ProcessChanges() /// The (combined) row from the change table and leases table /// Thrown if the value of the "SYS_CHANGE_OPERATION" column is none of "I", "U", or "D" /// SqlChangeOperation.Insert for an insert, SqlChangeOperation.Update for an update, and SqlChangeOperation.Delete for a delete - private static SqlChangeOperation GetChangeOperation(IReadOnlyDictionary row) + private static SqlChangeOperation GetChangeOperation(IReadOnlyDictionary row) { - string operation = row["SYS_CHANGE_OPERATION"]; + string operation = row["SYS_CHANGE_OPERATION"].ToString(); switch (operation) { case "I": return SqlChangeOperation.Insert; @@ -672,7 +687,7 @@ private SqlCommand BuildGetChangesCommand(SqlConnection connection, SqlTransacti FROM {SqlTriggerConstants.GlobalStateTableName} WHERE UserFunctionID = '{this._userFunctionId}' AND UserTableID = {this._userTableId}; - SELECT TOP {BatchSize} + SELECT TOP {this._batchSize} {selectList}, c.SYS_CHANGE_VERSION, c.SYS_CHANGE_OPERATION, l.{SqlTriggerConstants.LeasesTableChangeVersionColumnName}, l.{SqlTriggerConstants.LeasesTableAttemptCountColumnName}, l.{SqlTriggerConstants.LeasesTableLeaseExpirationTimeColumnName} @@ -683,7 +698,7 @@ LEFT OUTER JOIN {this._userTable.BracketQuotedFullName} AS u ON {userTableJoinCo (l.{SqlTriggerConstants.LeasesTableLeaseExpirationTimeColumnName} IS NULL AND (l.{SqlTriggerConstants.LeasesTableChangeVersionColumnName} IS NULL OR l.{SqlTriggerConstants.LeasesTableChangeVersionColumnName} < c.SYS_CHANGE_VERSION) OR l.{SqlTriggerConstants.LeasesTableLeaseExpirationTimeColumnName} < SYSDATETIME()) AND - (l.{SqlTriggerConstants.LeasesTableAttemptCountColumnName} IS NULL OR l.{SqlTriggerConstants.LeasesTableAttemptCountColumnName} < {MaxAttemptCount}) + (l.{SqlTriggerConstants.LeasesTableAttemptCountColumnName} IS NULL OR l.{SqlTriggerConstants.LeasesTableAttemptCountColumnName} < {MaxChangeProcessAttemptCount}) ORDER BY c.SYS_CHANGE_VERSION ASC; "; @@ -696,15 +711,16 @@ LEFT OUTER JOIN {this._userTable.BracketQuotedFullName} AS u ON {userTableJoinCo /// /// The connection to add to the returned SqlCommand /// The transaction to add to the returned SqlCommand + /// Dictionary representing the table rows on which leases should be acquired /// The SqlCommand populated with the query and appropriate parameters - private SqlCommand BuildAcquireLeasesCommand(SqlConnection connection, SqlTransaction transaction) + private SqlCommand BuildAcquireLeasesCommand(SqlConnection connection, SqlTransaction transaction, IReadOnlyList> rows) { var acquireLeasesQuery = new StringBuilder(); - for (int rowIndex = 0; rowIndex < this._rows.Count; rowIndex++) + for (int rowIndex = 0; rowIndex < rows.Count; rowIndex++) { string valuesList = string.Join(", ", this._primaryKeyColumns.Select((_, colIndex) => $"@{rowIndex}_{colIndex}")); - string changeVersion = this._rows[rowIndex]["SYS_CHANGE_VERSION"]; + string changeVersion = rows[rowIndex]["SYS_CHANGE_VERSION"].ToString(); acquireLeasesQuery.Append($@" IF NOT EXISTS (SELECT * FROM {this._leasesTableName} WITH (TABLOCKX) WHERE {this._rowMatchConditions[rowIndex]}) @@ -720,7 +736,7 @@ IF NOT EXISTS (SELECT * FROM {this._leasesTableName} WITH (TABLOCKX) WHERE {this "); } - return this.GetSqlCommandWithParameters(acquireLeasesQuery.ToString(), connection, transaction); + return this.GetSqlCommandWithParameters(acquireLeasesQuery.ToString(), connection, transaction, rows); } /// @@ -738,7 +754,7 @@ private SqlCommand BuildRenewLeasesCommand(SqlConnection connection) WHERE {matchCondition}; "; - return this.GetSqlCommandWithParameters(renewLeasesQuery, connection, null); + return this.GetSqlCommandWithParameters(renewLeasesQuery, connection, null, this._rows); } /// @@ -754,7 +770,7 @@ private SqlCommand BuildReleaseLeasesCommand(SqlConnection connection, SqlTransa for (int rowIndex = 0; rowIndex < this._rows.Count; rowIndex++) { - string changeVersion = this._rows[rowIndex]["SYS_CHANGE_VERSION"]; + string changeVersion = this._rows[rowIndex]["SYS_CHANGE_VERSION"].ToString(); releaseLeasesQuery.Append($@" SELECT @current_change_version = {SqlTriggerConstants.LeasesTableChangeVersionColumnName} @@ -771,7 +787,7 @@ private SqlCommand BuildReleaseLeasesCommand(SqlConnection connection, SqlTransa "); } - return this.GetSqlCommandWithParameters(releaseLeasesQuery.ToString(), connection, transaction); + return this.GetSqlCommandWithParameters(releaseLeasesQuery.ToString(), connection, transaction, this._rows); } /// @@ -803,7 +819,7 @@ FROM CHANGETABLE(CHANGES {this._userTable.BracketQuotedFullName}, @current_last_ ((l.{SqlTriggerConstants.LeasesTableChangeVersionColumnName} IS NULL OR l.{SqlTriggerConstants.LeasesTableChangeVersionColumnName} != c.SYS_CHANGE_VERSION OR l.{SqlTriggerConstants.LeasesTableLeaseExpirationTimeColumnName} IS NOT NULL) AND - (l.{SqlTriggerConstants.LeasesTableAttemptCountColumnName} IS NULL OR l.{SqlTriggerConstants.LeasesTableAttemptCountColumnName} < {MaxAttemptCount}))) AS Changes + (l.{SqlTriggerConstants.LeasesTableAttemptCountColumnName} IS NULL OR l.{SqlTriggerConstants.LeasesTableAttemptCountColumnName} < {MaxChangeProcessAttemptCount}))) AS Changes IF @unprocessed_changes = 0 AND @current_last_sync_version < {newLastSyncVersion} BEGIN @@ -827,6 +843,7 @@ FROM CHANGETABLE(CHANGES {this._userTable.BracketQuotedFullName}, @current_last_ /// SQL query string /// The connection to add to the returned SqlCommand /// The transaction to add to the returned SqlCommand + /// Dictionary representing the table rows /// /// Ideally, we would have a map that maps from rows to a list of SqlCommands populated with their primary key /// values. The issue with this is that SQL doesn't seem to allow adding parameters to one collection when they @@ -834,12 +851,13 @@ FROM CHANGETABLE(CHANGES {this._userTable.BracketQuotedFullName}, @current_last_ /// is thrown if they are also added to the collection of a SqlCommand. The expected behavior seems to be to /// rebuild the SqlParameters each time. /// - private SqlCommand GetSqlCommandWithParameters(string commandText, SqlConnection connection, SqlTransaction transaction) + private SqlCommand GetSqlCommandWithParameters(string commandText, SqlConnection connection, + SqlTransaction transaction, IReadOnlyList> rows) { var command = new SqlCommand(commandText, connection, transaction); - SqlParameter[] parameters = Enumerable.Range(0, this._rows.Count) - .SelectMany(rowIndex => this._primaryKeyColumns.Select((col, colIndex) => new SqlParameter($"@{rowIndex}_{colIndex}", this._rows[rowIndex][col]))) + SqlParameter[] parameters = Enumerable.Range(0, rows.Count) + .SelectMany(rowIndex => this._primaryKeyColumns.Select((col, colIndex) => new SqlParameter($"@{rowIndex}_{colIndex}", rows[rowIndex][col]))) .ToArray(); command.Parameters.AddRange(parameters); diff --git a/src/TriggerBinding/SqlTriggerBinding.cs b/src/TriggerBinding/SqlTriggerBinding.cs index b04fd95c4..841c8a79e 100644 --- a/src/TriggerBinding/SqlTriggerBinding.cs +++ b/src/TriggerBinding/SqlTriggerBinding.cs @@ -15,6 +15,7 @@ using Microsoft.Azure.WebJobs.Host.Protocols; using Microsoft.Azure.WebJobs.Host.Triggers; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Configuration; namespace Microsoft.Azure.WebJobs.Extensions.Sql { @@ -29,6 +30,7 @@ internal sealed class SqlTriggerBinding : ITriggerBinding private readonly ParameterInfo _parameter; private readonly IHostIdProvider _hostIdProvider; private readonly ILogger _logger; + private readonly IConfiguration _configuration; private static readonly IReadOnlyDictionary _emptyBindingContract = new Dictionary(); private static readonly IReadOnlyDictionary _emptyBindingData = new Dictionary(); @@ -41,13 +43,15 @@ internal sealed class SqlTriggerBinding : ITriggerBinding /// Trigger binding parameter information /// Provider of unique host identifier /// Facilitates logging of messages - public SqlTriggerBinding(string connectionString, string tableName, ParameterInfo parameter, IHostIdProvider hostIdProvider, ILogger logger) + /// Provides configuration values + public SqlTriggerBinding(string connectionString, string tableName, ParameterInfo parameter, IHostIdProvider hostIdProvider, ILogger logger, IConfiguration configuration) { this._connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); this._tableName = tableName ?? throw new ArgumentNullException(nameof(tableName)); this._parameter = parameter ?? throw new ArgumentNullException(nameof(parameter)); this._hostIdProvider = hostIdProvider ?? throw new ArgumentNullException(nameof(hostIdProvider)); this._logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this._configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); } /// @@ -68,7 +72,7 @@ public async Task CreateListenerAsync(ListenerFactoryContext context) _ = context ?? throw new ArgumentNullException(nameof(context), "Missing listener context"); string userFunctionId = await this.GetUserFunctionIdAsync(); - return new SqlTriggerListener(this._connectionString, this._tableName, userFunctionId, context.Executor, this._logger); + return new SqlTriggerListener(this._connectionString, this._tableName, userFunctionId, context.Executor, this._logger, this._configuration); } public ParameterDescriptor ToParameterDescriptor() diff --git a/src/TriggerBinding/SqlTriggerBindingProvider.cs b/src/TriggerBinding/SqlTriggerBindingProvider.cs index 2fc6644ca..17afa4cc3 100644 --- a/src/TriggerBinding/SqlTriggerBindingProvider.cs +++ b/src/TriggerBinding/SqlTriggerBindingProvider.cs @@ -33,8 +33,7 @@ public SqlTriggerBindingProvider(IConfiguration configuration, IHostIdProvider h this._configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); this._hostIdProvider = hostIdProvider ?? throw new ArgumentNullException(nameof(hostIdProvider)); - _ = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); - this._logger = loggerFactory.CreateLogger(LogCategories.CreateTriggerCategory("Sql")); + this._logger = loggerFactory?.CreateLogger(LogCategories.CreateTriggerCategory("Sql")) ?? throw new ArgumentNullException(nameof(loggerFactory)); } /// @@ -79,10 +78,10 @@ public Task TryCreateAsync(TriggerBindingProviderContext contex Type userType = parameter.ParameterType.GetGenericArguments()[0].GetGenericArguments()[0]; Type bindingType = typeof(SqlTriggerBinding<>).MakeGenericType(userType); - var constructorParameterTypes = new Type[] { typeof(string), typeof(string), typeof(ParameterInfo), typeof(IHostIdProvider), typeof(ILogger) }; + var constructorParameterTypes = new Type[] { typeof(string), typeof(string), typeof(ParameterInfo), typeof(IHostIdProvider), typeof(ILogger), typeof(IConfiguration) }; ConstructorInfo bindingConstructor = bindingType.GetConstructor(constructorParameterTypes); - object[] constructorParameterValues = new object[] { connectionString, attribute.TableName, parameter, this._hostIdProvider, this._logger }; + object[] constructorParameterValues = new object[] { connectionString, attribute.TableName, parameter, this._hostIdProvider, this._logger, this._configuration }; var triggerBinding = (ITriggerBinding)bindingConstructor.Invoke(constructorParameterValues); return Task.FromResult(triggerBinding); diff --git a/src/TriggerBinding/SqlTriggerConstants.cs b/src/TriggerBinding/SqlTriggerConstants.cs index 2f37d866f..8c627202d 100644 --- a/src/TriggerBinding/SqlTriggerConstants.cs +++ b/src/TriggerBinding/SqlTriggerConstants.cs @@ -14,5 +14,8 @@ internal static class SqlTriggerConstants public const string LeasesTableChangeVersionColumnName = "_az_func_ChangeVersion"; public const string LeasesTableAttemptCountColumnName = "_az_func_AttemptCount"; public const string LeasesTableLeaseExpirationTimeColumnName = "_az_func_LeaseExpirationTime"; + + public const string ConfigKey_SqlTrigger_BatchSize = "Sql_Trigger_BatchSize"; + public const string ConfigKey_SqlTrigger_PollingInterval = "Sql_Trigger_PollingIntervalMs"; } } \ No newline at end of file diff --git a/src/TriggerBinding/SqlTriggerListener.cs b/src/TriggerBinding/SqlTriggerListener.cs index 5523ffc69..d1d95bb7d 100644 --- a/src/TriggerBinding/SqlTriggerListener.cs +++ b/src/TriggerBinding/SqlTriggerListener.cs @@ -14,6 +14,7 @@ using Microsoft.Azure.WebJobs.Host.Listeners; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Configuration; namespace Microsoft.Azure.WebJobs.Extensions.Sql { @@ -34,11 +35,12 @@ internal sealed class SqlTriggerListener : IListener private readonly string _userFunctionId; private readonly ITriggeredFunctionExecutor _executor; private readonly ILogger _logger; + private readonly IConfiguration _configuration; private readonly IDictionary _telemetryProps = new Dictionary(); private SqlTableChangeMonitor _changeMonitor; - private int _listenerState; + private int _listenerState = ListenerNotStarted; /// /// Initializes a new instance of the class. @@ -48,20 +50,15 @@ internal sealed class SqlTriggerListener : IListener /// Unique identifier for the user function /// Defines contract for triggering user function /// Facilitates logging of messages - public SqlTriggerListener(string connectionString, string tableName, string userFunctionId, ITriggeredFunctionExecutor executor, ILogger logger) + /// Provides configuration values + public SqlTriggerListener(string connectionString, string tableName, string userFunctionId, ITriggeredFunctionExecutor executor, ILogger logger, IConfiguration configuration) { - _ = !string.IsNullOrEmpty(connectionString) ? true : throw new ArgumentNullException(nameof(connectionString)); - _ = !string.IsNullOrEmpty(tableName) ? true : throw new ArgumentNullException(nameof(tableName)); - _ = !string.IsNullOrEmpty(userFunctionId) ? true : throw new ArgumentNullException(nameof(userFunctionId)); - _ = executor ?? throw new ArgumentNullException(nameof(executor)); - _ = logger ?? throw new ArgumentNullException(nameof(logger)); - - this._connectionString = connectionString; - this._userTable = new SqlObject(tableName); - this._userFunctionId = userFunctionId; - this._executor = executor; - this._logger = logger; - this._listenerState = ListenerNotStarted; + this._connectionString = !string.IsNullOrEmpty(connectionString) ? connectionString : throw new ArgumentNullException(nameof(connectionString)); + this._userTable = !string.IsNullOrEmpty(tableName) ? new SqlObject(tableName) : throw new ArgumentNullException(nameof(tableName)); + this._userFunctionId = !string.IsNullOrEmpty(userFunctionId) ? userFunctionId : throw new ArgumentNullException(nameof(userFunctionId)); + this._executor = executor ?? throw new ArgumentNullException(nameof(executor)); + this._logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this._configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); } public void Cancel() @@ -129,6 +126,7 @@ public async Task StartAsync(CancellationToken cancellationToken) primaryKeyColumns.Select(col => col.name).ToList(), this._executor, this._logger, + this._configuration, this._telemetryProps); this._listenerState = ListenerStarted; @@ -474,7 +472,7 @@ PRIMARY KEY ({primaryKeys}) } /// - /// Clears the current telemetry property dictionary and initializes the default initial properties. + /// Clears the current telemetry property dictionary and initializes the default initial properties. /// private void InitializeTelemetryProps() { diff --git a/src/Utils.cs b/src/Utils.cs index d05d01a9a..0bff69855 100644 --- a/src/Utils.cs +++ b/src/Utils.cs @@ -99,5 +99,10 @@ public static void LogDebugWithThreadId(this ILogger logger, string message, par { logger.LogDebug($"TID:{Environment.CurrentManagedThreadId} {message}", args); } + + public static void LogInformationWithThreadId(this ILogger logger, string message, params object[] args) + { + logger.LogInformation($"TID:{Environment.CurrentManagedThreadId} {message}", args); + } } } diff --git a/test/.editorconfig b/test/.editorconfig index 4386dd31a..0082aa2a0 100644 --- a/test/.editorconfig +++ b/test/.editorconfig @@ -5,4 +5,5 @@ # Disabled dotnet_diagnostic.CA1309.severity = silent # Use ordinal StringComparison - this isn't important for tests and just adds clutter dotnet_diagnostic.CA1305.severity = silent # Specify IFormatProvider - this isn't important for tests and just adds clutter -dotnet_diagnostic.CA1707.severity = silent # Identifiers should not contain underscores - this helps make test names more readable \ No newline at end of file +dotnet_diagnostic.CA1707.severity = silent # Identifiers should not contain underscores - this helps make test names more readable +dotnet_diagnostic.CA2201.severity = silent # Do not raise reserved exception types - tests can throw whatever they want \ No newline at end of file diff --git a/test/Common/ProductColumnTypes.cs b/test/Common/ProductColumnTypes.cs new file mode 100644 index 000000000..97e36eb0b --- /dev/null +++ b/test/Common/ProductColumnTypes.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Common +{ + public class ProductColumnTypes + { + public int ProductID { get; set; } + + public DateTime Datetime { get; set; } + + public DateTime Datetime2 { get; set; } + } +} diff --git a/test/Common/TestUtils.cs b/test/Common/TestUtils.cs index 1d302a1c1..38330e33d 100644 --- a/test/Common/TestUtils.cs +++ b/test/Common/TestUtils.cs @@ -60,6 +60,7 @@ public static int ExecuteNonQuery( cmd.CommandText = commandText; cmd.CommandType = CommandType.Text; + cmd.CommandTimeout = 60000; // Increase from default 30s to prevent timeouts while connecting to Azure SQL DB Console.WriteLine($"Executing non-query {commandText}"); return cmd.ExecuteNonQuery(); } diff --git a/test/Database/Tables/ProductsColumnTypes.sql b/test/Database/Tables/ProductsColumnTypes.sql new file mode 100644 index 000000000..a552b9db1 --- /dev/null +++ b/test/Database/Tables/ProductsColumnTypes.sql @@ -0,0 +1,5 @@ +CREATE TABLE [ProductsColumnTypes] ( + [ProductId] [int] NOT NULL PRIMARY KEY, + [Datetime] [datetime], + [Datetime2] [datetime2] +) \ No newline at end of file diff --git a/test/Integration/test-csharp/Database/Tables/ProductsWithReservedPrimaryKeyColumnNames.sql b/test/Database/Tables/ProductsWithReservedPrimaryKeyColumnNames.sql similarity index 100% rename from test/Integration/test-csharp/Database/Tables/ProductsWithReservedPrimaryKeyColumnNames.sql rename to test/Database/Tables/ProductsWithReservedPrimaryKeyColumnNames.sql diff --git a/test/Integration/test-csharp/Database/Tables/ProductsWithUnsupportedColumnTypes.sql b/test/Database/Tables/ProductsWithUnsupportedColumnTypes.sql similarity index 100% rename from test/Integration/test-csharp/Database/Tables/ProductsWithUnsupportedColumnTypes.sql rename to test/Database/Tables/ProductsWithUnsupportedColumnTypes.sql diff --git a/test/Integration/test-csharp/Database/Tables/ProductsWithoutPrimaryKey.sql b/test/Database/Tables/ProductsWithoutPrimaryKey.sql similarity index 100% rename from test/Integration/test-csharp/Database/Tables/ProductsWithoutPrimaryKey.sql rename to test/Database/Tables/ProductsWithoutPrimaryKey.sql diff --git a/test/GlobalSuppressions.cs b/test/GlobalSuppressions.cs index 56034261e..b0fa905ab 100644 --- a/test/GlobalSuppressions.cs +++ b/test/GlobalSuppressions.cs @@ -17,3 +17,5 @@ [assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Unused parameter is required by functions binding", Scope = "member", Target = "~M:Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Integration.ReservedPrimaryKeyColumnNamesTrigger.Run(System.Collections.Generic.IReadOnlyList{Microsoft.Azure.WebJobs.Extensions.Sql.SqlChange{Microsoft.Azure.WebJobs.Extensions.Sql.Samples.Common.Product})")] [assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Unused parameter is required by functions binding", Scope = "member", Target = "~M:Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Integration.TableNotPresentTrigger.Run(System.Collections.Generic.IReadOnlyList{Microsoft.Azure.WebJobs.Extensions.Sql.SqlChange{Microsoft.Azure.WebJobs.Extensions.Sql.Samples.Common.Product})")] [assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Unused parameter is required by functions binding", Scope = "member", Target = "~M:Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Integration.UnsupportedColumnTypesTrigger.Run(System.Collections.Generic.IReadOnlyList{Microsoft.Azure.WebJobs.Extensions.Sql.SqlChange{Microsoft.Azure.WebJobs.Extensions.Sql.Samples.Common.Product})")] +[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "", Scope = "member", Target = "~M:Microsoft.Azure.WebJobs.Extensions.Sql.Samples.InputBindingSamples.GetProductsColumnTypesSerializationDifferentCulture.Run(Microsoft.AspNetCore.Http.HttpRequest,System.Collections.Generic.IAsyncEnumerable{Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Common.ProductColumnTypes},Microsoft.Extensions.Logging.ILogger)~System.Threading.Tasks.Task{Microsoft.AspNetCore.Mvc.IActionResult}")] +[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "", Scope = "member", Target = "~M:Microsoft.Azure.WebJobs.Extensions.Sql.Samples.InputBindingSamples.GetProductsColumnTypesSerialization.Run(Microsoft.AspNetCore.Http.HttpRequest,System.Collections.Generic.IAsyncEnumerable{Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Common.ProductColumnTypes},Microsoft.Extensions.Logging.ILogger)~System.Threading.Tasks.Task{Microsoft.AspNetCore.Mvc.IActionResult}")] diff --git a/test/Integration/IntegrationTestBase.cs b/test/Integration/IntegrationTestBase.cs index 444feedf1..63cdac911 100644 --- a/test/Integration/IntegrationTestBase.cs +++ b/test/Integration/IntegrationTestBase.cs @@ -15,6 +15,9 @@ using Xunit; using Xunit.Abstractions; using Microsoft.Azure.WebJobs.Extensions.Sql.Samples.Common; +using Microsoft.AspNetCore.WebUtilities; +using System.Collections.Generic; +using System.Linq; namespace Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Integration { @@ -48,7 +51,7 @@ public class IntegrationTestBase : IDisposable /// /// Output redirect for XUnit tests. - /// Please use TestOutput.WriteLine() instead of Console or Debug. + /// Please use LogOutput() instead of Console or Debug. /// protected ITestOutputHelper TestOutput { get; private set; } @@ -57,7 +60,7 @@ public class IntegrationTestBase : IDisposable /// protected int Port { get; private set; } = 7071; - public IntegrationTestBase(ITestOutputHelper output) + public IntegrationTestBase(ITestOutputHelper output = null) { this.TestOutput = output; this.SetupDatabase(); @@ -74,35 +77,44 @@ public IntegrationTestBase(ITestOutputHelper output) /// private void SetupDatabase() { - // Get the test server name from environment variable "TEST_SERVER", default to localhost if not set - string testServer = Environment.GetEnvironmentVariable("TEST_SERVER"); - if (string.IsNullOrEmpty(testServer)) + SqlConnectionStringBuilder connectionStringBuilder; + string connectionString = Environment.GetEnvironmentVariable("TEST_CONNECTION_STRING"); + if (connectionString != null) { - testServer = "localhost"; - } - - // First connect to master to create the database - var connectionStringBuilder = new SqlConnectionStringBuilder() - { - DataSource = testServer, - InitialCatalog = "master", - Pooling = false - }; - - // Either use integrated auth or SQL login depending if SA_PASSWORD is set - string userId = "SA"; - string password = Environment.GetEnvironmentVariable("SA_PASSWORD"); - if (string.IsNullOrEmpty(password)) - { - connectionStringBuilder.IntegratedSecurity = true; + this.MasterConnectionString = connectionString; + connectionStringBuilder = new SqlConnectionStringBuilder(connectionString); } else { - connectionStringBuilder.UserID = userId; - connectionStringBuilder.Password = password; - } + // Get the test server name from environment variable "TEST_SERVER", default to localhost if not set + string testServer = Environment.GetEnvironmentVariable("TEST_SERVER"); + if (string.IsNullOrEmpty(testServer)) + { + testServer = "localhost"; + } - this.MasterConnectionString = connectionStringBuilder.ToString(); + // First connect to master to create the database + connectionStringBuilder = new SqlConnectionStringBuilder() + { + DataSource = testServer, + InitialCatalog = "master", + Pooling = false + }; + + // Either use integrated auth or SQL login depending if SA_PASSWORD is set + string userId = "SA"; + string password = Environment.GetEnvironmentVariable("SA_PASSWORD"); + if (string.IsNullOrEmpty(password)) + { + connectionStringBuilder.IntegratedSecurity = true; + } + else + { + connectionStringBuilder.UserID = userId; + connectionStringBuilder.Password = password; + } + this.MasterConnectionString = connectionStringBuilder.ToString(); + } // Create database // Retry this in case the server isn't fully initialized yet @@ -134,7 +146,7 @@ private void ExecuteAllScriptsInFolder(string folder) { foreach (string file in Directory.EnumerateFiles(folder, "*.sql")) { - Console.WriteLine($"Executing script ${file}"); + this.LogOutput($"Executing script ${file}"); this.ExecuteNonQuery(File.ReadAllText(file)); } } @@ -164,7 +176,7 @@ protected void StartAzurite() /// - The functionName is different than its route.
/// - You can start multiple functions by passing in a space-separated list of function names.
/// - protected void StartFunctionHost(string functionName, SupportedLanguages language, bool useTestFolder = false, DataReceivedEventHandler customOutputHandler = null) + protected void StartFunctionHost(string functionName, SupportedLanguages language, bool useTestFolder = false, DataReceivedEventHandler customOutputHandler = null, IDictionary environmentVariables = null) { string workingDirectory = useTestFolder ? GetPathToBin() : Path.Combine(GetPathToBin(), "SqlExtensionSamples", Enum.GetName(typeof(SupportedLanguages), language)); if (!Directory.Exists(workingDirectory)) @@ -183,7 +195,11 @@ protected void StartFunctionHost(string functionName, SupportedLanguages languag RedirectStandardError = true, UseShellExecute = false }; - this.TestOutput.WriteLine($"Starting {startInfo.FileName} {startInfo.Arguments} in {startInfo.WorkingDirectory}"); + if (environmentVariables != null) + { + environmentVariables.ToList().ForEach(ev => startInfo.EnvironmentVariables[ev.Key] = ev.Value); + } + this.LogOutput($"Starting {startInfo.FileName} {startInfo.Arguments} in {startInfo.WorkingDirectory}"); this.FunctionHost = new Process { StartInfo = startInfo @@ -201,7 +217,7 @@ protected void StartFunctionHost(string functionName, SupportedLanguages languag this.FunctionHost.BeginOutputReadLine(); this.FunctionHost.BeginErrorReadLine(); - this.TestOutput.WriteLine($"Waiting for Azure Function host to start..."); + this.LogOutput($"Waiting for Azure Function host to start..."); const int FunctionHostStartupTimeoutInSeconds = 60; bool isCompleted = taskCompletionSource.Task.Wait(TimeSpan.FromSeconds(FunctionHostStartupTimeoutInSeconds)); @@ -212,7 +228,7 @@ protected void StartFunctionHost(string functionName, SupportedLanguages languag const int BufferTimeInSeconds = 5; Task.Delay(TimeSpan.FromSeconds(BufferTimeInSeconds)).Wait(); - this.TestOutput.WriteLine($"Azure Function host started!"); + this.LogOutput($"Azure Function host started!"); this.FunctionHost.OutputDataReceived -= SignalStartupHandler; void SignalStartupHandler(object sender, DataReceivedEventArgs e) @@ -223,7 +239,8 @@ void SignalStartupHandler(object sender, DataReceivedEventArgs e) { taskCompletionSource.SetResult(true); } - } + }; + taskCompletionSource.Task.Wait(60000); } private static string GetFunctionsCoreToolsPath() @@ -259,18 +276,30 @@ private static string GetFunctionsCoreToolsPath() return funcPath; } + private void LogOutput(string output) + { + if (this.TestOutput != null) + { + this.TestOutput.WriteLine(output); + } + else + { + Console.WriteLine(output); + } + } + private void TestOutputHandler(object sender, DataReceivedEventArgs e) { if (e != null && !string.IsNullOrEmpty(e.Data)) { - this.TestOutput.WriteLine(e.Data); + this.LogOutput(e.Data); } } protected async Task SendGetRequest(string requestUri, bool verifySuccess = true) { string timeStamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", System.Globalization.CultureInfo.InvariantCulture); - this.TestOutput.WriteLine($"[{timeStamp}] Sending GET request: {requestUri}"); + this.LogOutput($"[{timeStamp}] Sending GET request: {requestUri}"); if (string.IsNullOrEmpty(requestUri)) { @@ -290,7 +319,7 @@ protected async Task SendGetRequest(string requestUri, bool protected async Task SendPostRequest(string requestUri, string json, bool verifySuccess = true) { - this.TestOutput.WriteLine("Sending POST request: " + requestUri); + this.LogOutput("Sending POST request: " + requestUri); if (string.IsNullOrEmpty(requestUri)) { @@ -340,7 +369,7 @@ public void Dispose() } catch (Exception e1) { - this.TestOutput.WriteLine($"Failed to close connection. Error: {e1.Message}"); + this.LogOutput($"Failed to close connection. Error: {e1.Message}"); } try @@ -350,7 +379,7 @@ public void Dispose() } catch (Exception e2) { - this.TestOutput.WriteLine($"Failed to stop function host, Error: {e2.Message}"); + this.LogOutput($"Failed to stop function host, Error: {e2.Message}"); } try @@ -360,7 +389,7 @@ public void Dispose() } catch (Exception e3) { - this.TestOutput.WriteLine($"Failed to stop Azurite, Error: {e3.Message}"); + this.LogOutput($"Failed to stop Azurite, Error: {e3.Message}"); } try @@ -372,10 +401,97 @@ public void Dispose() } catch (Exception e4) { - this.TestOutput.WriteLine($"Failed to drop {this.DatabaseName}, Error: {e4.Message}"); + this.LogOutput($"Failed to drop {this.DatabaseName}, Error: {e4.Message}"); } GC.SuppressFinalize(this); } + + protected async Task SendInputRequest(string functionName, string query = "") + { + string requestUri = $"http://localhost:{this.Port}/api/{functionName}/{query}"; + + return await this.SendGetRequest(requestUri); + } + + protected Task SendOutputGetRequest(string functionName, IDictionary query = null) + { + string requestUri = $"http://localhost:{this.Port}/api/{functionName}"; + + if (query != null) + { + requestUri = QueryHelpers.AddQueryString(requestUri, query); + } + + return this.SendGetRequest(requestUri); + } + + protected Task SendOutputPostRequest(string functionName, string query) + { + string requestUri = $"http://localhost:{this.Port}/api/{functionName}"; + + return this.SendPostRequest(requestUri, query); + } + + protected void InsertProducts(Product[] products) + { + if (products.Length == 0) + { + return; + } + + var queryBuilder = new StringBuilder(); + foreach (Product p in products) + { + queryBuilder.AppendLine($"INSERT INTO dbo.Products VALUES({p.ProductID}, '{p.Name}', {p.Cost});"); + } + + this.ExecuteNonQuery(queryBuilder.ToString()); + } + + protected static Product[] GetProducts(int n, int cost) + { + var result = new Product[n]; + for (int i = 1; i <= n; i++) + { + result[i - 1] = new Product + { + ProductID = i, + Name = "test", + Cost = cost * i + }; + } + return result; + } + + protected static Product[] GetProductsWithSameCost(int n, int cost) + { + var result = new Product[n]; + for (int i = 0; i < n; i++) + { + result[i] = new Product + { + ProductID = i, + Name = "test", + Cost = cost + }; + } + return result; + } + + protected static Product[] GetProductsWithSameCostAndName(int n, int cost, string name, int offset = 0) + { + var result = new Product[n]; + for (int i = 0; i < n; i++) + { + result[i] = new Product + { + ProductID = i + offset, + Name = name, + Cost = cost + }; + } + return result; + } } } diff --git a/test/Integration/SqlInputBindingIntegrationTests.cs b/test/Integration/SqlInputBindingIntegrationTests.cs index d5750daa5..7b63908a1 100644 --- a/test/Integration/SqlInputBindingIntegrationTests.cs +++ b/test/Integration/SqlInputBindingIntegrationTests.cs @@ -3,8 +3,6 @@ using System; using System.Net.Http; -using System.Text; -using System.Threading.Tasks; using Newtonsoft.Json; using Microsoft.Azure.WebJobs.Extensions.Sql.Samples.Common; using Microsoft.Azure.WebJobs.Extensions.Sql.Samples.InputBindingSamples; @@ -21,13 +19,6 @@ public SqlInputBindingIntegrationTests(ITestOutputHelper output) : base(output) { } - private async Task SendInputRequest(string functionName, string query = "") - { - string requestUri = $"http://localhost:{this.Port}/api/{functionName}/{query}"; - - return await this.SendGetRequest(requestUri); - } - [Theory] [SqlInlineData(0, 100)] [SqlInlineData(1, -500)] @@ -140,65 +131,44 @@ public async void GetProductNamesViewTest(SupportedLanguages lang) Assert.Equal(expectedResponse, TestUtils.CleanJsonString(actualResponse), StringComparer.OrdinalIgnoreCase); } - private static Product[] GetProductsWithSameCost(int n, int cost) + /// + /// Verifies that serializing an item with various data types works when the language is + /// set to a non-enUS language. + /// + [Theory] + [SqlInlineData()] + [UnsupportedLanguages(SupportedLanguages.JavaScript)] // Javascript doesn't have the concept of a runtime language used during serialization + public async void GetProductsColumnTypesSerializationDifferentCultureTest(SupportedLanguages lang) { - var result = new Product[n]; - for (int i = 0; i < n; i++) - { - result[i] = new Product - { - ProductID = i, - Name = "test", - Cost = cost - }; - } - return result; - } + this.StartFunctionHost(nameof(GetProductsColumnTypesSerializationDifferentCulture), lang, true); - private static Product[] GetProducts(int n, int cost) - { - var result = new Product[n]; - for (int i = 1; i <= n; i++) - { - result[i - 1] = new Product - { - ProductID = i, - Name = "test", - Cost = cost * i - }; - } - return result; - } + this.ExecuteNonQuery("INSERT INTO [dbo].[ProductsColumnTypes] VALUES (" + + "999, " + // ProductId + "GETDATE(), " + // Datetime field + "GETDATE())"); // Datetime2 field - private static Product[] GetProductsWithSameCostAndName(int n, int cost, string name, int offset = 0) - { - var result = new Product[n]; - for (int i = 0; i < n; i++) - { - result[i] = new Product - { - ProductID = i + offset, - Name = name, - Cost = cost - }; - } - return result; + await this.SendInputRequest("getproducts-columntypesserializationdifferentculture"); + + // If we get here the test has succeeded - it'll throw an exception if serialization fails } - private void InsertProducts(Product[] products) + /// + /// Verifies that serializing an item with various data types works as expected + /// + [Theory] + [SqlInlineData()] + public async void GetProductsColumnTypesSerializationTest(SupportedLanguages lang) { - if (products.Length == 0) - { - return; - } - - var queryBuilder = new StringBuilder(); - foreach (Product p in products) - { - queryBuilder.AppendLine($"INSERT INTO dbo.Products VALUES({p.ProductID}, '{p.Name}', {p.Cost});"); - } - - this.ExecuteNonQuery(queryBuilder.ToString()); + this.StartFunctionHost(nameof(GetProductsColumnTypesSerialization), lang, true); + + this.ExecuteNonQuery("INSERT INTO [dbo].[ProductsColumnTypes] VALUES (" + + "999, " + // ProductId + "GETDATE(), " + // Datetime field + "GETDATE())"); // Datetime2 field + + await this.SendInputRequest("getproducts-columntypesserialization"); + + // If we get here the test has succeeded - it'll throw an exception if serialization fails } } } diff --git a/test/Integration/SqlOutputBindingIntegrationTests.cs b/test/Integration/SqlOutputBindingIntegrationTests.cs index d8423f5c5..5b2d17d63 100644 --- a/test/Integration/SqlOutputBindingIntegrationTests.cs +++ b/test/Integration/SqlOutputBindingIntegrationTests.cs @@ -3,10 +3,7 @@ using System; using System.Collections.Generic; -using System.Net.Http; using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.WebUtilities; using Microsoft.Azure.WebJobs.Extensions.Sql.Samples.OutputBindingSamples; using Microsoft.Azure.WebJobs.Extensions.Sql.Samples.Common; using Xunit; @@ -23,25 +20,6 @@ public SqlOutputBindingIntegrationTests(ITestOutputHelper output) : base(output) { } - private Task SendOutputGetRequest(string functionName, IDictionary query = null) - { - string requestUri = $"http://localhost:{this.Port}/api/{functionName}"; - - if (query != null) - { - requestUri = QueryHelpers.AddQueryString(requestUri, query); - } - - return this.SendGetRequest(requestUri); - } - - private Task SendOutputPostRequest(string functionName, string query) - { - string requestUri = $"http://localhost:{this.Port}/api/{functionName}"; - - return this.SendPostRequest(requestUri, query); - } - [Theory] [SqlInlineData(1, "Test", 5)] [SqlInlineData(0, "", 0)] @@ -121,6 +99,27 @@ public void AddProductArrayTest(SupportedLanguages lang) Assert.Equal(2, this.ExecuteScalar("SELECT ProductId FROM Products WHERE Cost = 12")); } + /// + /// Test compatability with converting various data types to their respective + /// SQL server types. + /// + /// The language to run the test against + [Theory] + [SqlInlineData()] + public void AddProductColumnTypesTest(SupportedLanguages lang) + { + this.StartFunctionHost(nameof(AddProductColumnTypes), lang, true); + + var queryParameters = new Dictionary() + { + { "productId", "999" } + }; + + this.SendOutputGetRequest("addproduct-columntypes", queryParameters).Wait(); + + // If we get here then the test is successful - an exception will be thrown if there were any problems + } + [Theory] [SqlInlineData()] [UnsupportedLanguages(SupportedLanguages.JavaScript)] // Collectors are only available in C# diff --git a/test/Integration/SqlTriggerBindingIntegrationTests.cs b/test/Integration/SqlTriggerBindingIntegrationTests.cs index c92ab7a83..7eca03418 100644 --- a/test/Integration/SqlTriggerBindingIntegrationTests.cs +++ b/test/Integration/SqlTriggerBindingIntegrationTests.cs @@ -23,8 +23,7 @@ public SqlTriggerBindingIntegrationTests(ITestOutputHelper output) : base(output } /// - /// Ensures that the user function gets invoked for each of the insert, update and delete operation, and the - /// changes to the user table are passed to the function in correct sequence. + /// Ensures that the user function gets invoked for each of the insert, update and delete operation. /// [Fact] public async Task SingleOperationTriggerTest() @@ -33,7 +32,7 @@ public async Task SingleOperationTriggerTest() this.StartFunctionHost(nameof(ProductsTrigger), Common.SupportedLanguages.CSharp); var changes = new List>(); - this.MonitorProductChanges(changes); + this.MonitorProductChanges(changes, "SQL Changes: "); // Considering the polling interval of 5 seconds and batch-size of 10, it should take around 15 seconds to // process 30 insert operations. Similar reasoning is used to set delays for update and delete operations. @@ -56,6 +55,49 @@ public async Task SingleOperationTriggerTest() changes.Clear(); } + /// + /// Verifies that manually setting the batch size correctly changes the number of changes processed at once + /// + [Fact] + public async Task BatchSizeOverrideTriggerTest() + { + this.EnableChangeTrackingForTable("Products"); + this.StartFunctionHost(nameof(ProductsTriggerWithValidation), Common.SupportedLanguages.CSharp, true, environmentVariables: new Dictionary() { + { "TEST_EXPECTED_BATCH_SIZE", "20" }, + { "Sql_Trigger_BatchSize", "20" } + }); + + var changes = new List>(); + this.MonitorProductChanges(changes, "SQL Changes: "); + + // Considering the polling interval of 5 seconds and batch-size of 20, it should take around 10 seconds to + // process 40 insert operations. + this.InsertProducts(1, 40); + await Task.Delay(TimeSpan.FromSeconds(12)); + ValidateProductChanges(changes, 1, 40, SqlChangeOperation.Insert, id => $"Product {id}", id => id * 100); + } + + /// + /// Verifies that manually setting the polling interval correctly changes the delay between processing each batch of changes + /// + [Fact] + public async Task PollingIntervalOverrideTriggerTest() + { + this.EnableChangeTrackingForTable("Products"); + this.StartFunctionHost(nameof(ProductsTriggerWithValidation), Common.SupportedLanguages.CSharp, true, environmentVariables: new Dictionary() { + { "Sql_Trigger_PollingIntervalMs", "100" } + }); + + var changes = new List>(); + this.MonitorProductChanges(changes, "SQL Changes: "); + + // Considering the polling interval of 100ms and batch-size of 10, it should take around .5 second to + // process 50 insert operations. + this.InsertProducts(1, 50); + await Task.Delay(TimeSpan.FromSeconds(1)); + ValidateProductChanges(changes, 1, 50, SqlChangeOperation.Insert, id => $"Product {id}", id => id * 100); + } + /// /// Verifies that if several changes have happened to the table row since last invocation, then a single net @@ -68,7 +110,7 @@ public async Task MultiOperationTriggerTest() this.StartFunctionHost(nameof(ProductsTrigger), Common.SupportedLanguages.CSharp); var changes = new List>(); - this.MonitorProductChanges(changes); + this.MonitorProductChanges(changes, "SQL Changes: "); // Insert + multiple updates to a row are treated as single insert with latest row values. this.InsertProducts(1, 5); @@ -97,6 +139,52 @@ public async Task MultiOperationTriggerTest() changes.Clear(); } + + /// + /// Ensures correct functionality with multiple user functions tracking the same table. + /// + [Fact] + public async Task MultiFunctionTriggerTest() + { + this.EnableChangeTrackingForTable("Products"); + + string functionList = $"{nameof(MultiFunctionTrigger.MultiFunctionTrigger1)} {nameof(MultiFunctionTrigger.MultiFunctionTrigger2)}"; + this.StartFunctionHost(functionList, Common.SupportedLanguages.CSharp, useTestFolder: true); + + var changes1 = new List>(); + var changes2 = new List>(); + + this.MonitorProductChanges(changes1, "Trigger1 Changes: "); + this.MonitorProductChanges(changes2, "Trigger2 Changes: "); + + // Considering the polling interval of 5 seconds and batch-size of 10, it should take around 15 seconds to + // process 30 insert operations for each trigger-listener. Similar reasoning is used to set delays for + // update and delete operations. + this.InsertProducts(1, 30); + await Task.Delay(TimeSpan.FromSeconds(20)); + ValidateProductChanges(changes1, 1, 30, SqlChangeOperation.Insert, id => $"Product {id}", id => id * 100); + ValidateProductChanges(changes2, 1, 30, SqlChangeOperation.Insert, id => $"Product {id}", id => id * 100); + changes1.Clear(); + changes2.Clear(); + + // All table columns (not just the columns that were updated) would be returned for update operation. + this.UpdateProducts(1, 20); + await Task.Delay(TimeSpan.FromSeconds(15)); + ValidateProductChanges(changes1, 1, 20, SqlChangeOperation.Update, id => $"Updated Product {id}", id => id * 100); + ValidateProductChanges(changes2, 1, 20, SqlChangeOperation.Update, id => $"Updated Product {id}", id => id * 100); + changes1.Clear(); + changes2.Clear(); + + // The properties corresponding to non-primary key columns would be set to the C# type's default values + // (null and 0) for delete operation. + this.DeleteProducts(11, 30); + await Task.Delay(TimeSpan.FromSeconds(15)); + ValidateProductChanges(changes1, 11, 30, SqlChangeOperation.Delete, _ => null, _ => 0); + ValidateProductChanges(changes2, 11, 30, SqlChangeOperation.Delete, _ => null, _ => 0); + changes1.Clear(); + changes2.Clear(); + } + /// /// Tests the error message when the user table is not present in the database. /// @@ -177,16 +265,15 @@ ALTER TABLE [dbo].[{tableName}] "); } - private void MonitorProductChanges(List> changes) + private void MonitorProductChanges(List> changes, string messagePrefix) { int index = 0; - string prefix = "SQL Changes: "; this.FunctionHost.OutputDataReceived += (sender, e) => { - if (e.Data != null && (index = e.Data.IndexOf(prefix, StringComparison.Ordinal)) >= 0) + if (e.Data != null && (index = e.Data.IndexOf(messagePrefix, StringComparison.Ordinal)) >= 0) { - string json = e.Data[(index + prefix.Length)..]; + string json = e.Data[(index + messagePrefix.Length)..]; changes.AddRange(JsonConvert.DeserializeObject>>(json)); } }; @@ -269,9 +356,15 @@ void OutputHandler(object sender, DataReceivedEventArgs e) // All trigger integration tests are only using C# functions for testing at the moment. this.StartFunctionHost(functionName, Common.SupportedLanguages.CSharp, useTestFolder, OutputHandler); + + // The functions host generally logs the error message within a second after starting up. + const int BufferTimeForErrorInSeconds = 15; + bool isCompleted = tcs.Task.Wait(TimeSpan.FromSeconds(BufferTimeForErrorInSeconds)); + this.FunctionHost.OutputDataReceived -= OutputHandler; this.FunctionHost.Kill(); + Assert.True(isCompleted, "Functions host did not log failure to start SQL trigger listener within specified time."); Assert.Equal(expectedErrorMessage, errorMessage); } } diff --git a/test/Integration/test-csharp/AddProductColumnTypes.cs b/test/Integration/test-csharp/AddProductColumnTypes.cs new file mode 100644 index 000000000..180290ab1 --- /dev/null +++ b/test/Integration/test-csharp/AddProductColumnTypes.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Common; + +namespace Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Integration +{ + public static class AddProductColumnTypes + { + /// + /// This function is used to test compatability with converting various data types to their respective + /// SQL server types. + /// + [FunctionName(nameof(AddProductColumnTypes))] + public static IActionResult Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "addproduct-columntypes")] HttpRequest req, + [Sql("dbo.ProductsColumnTypes", ConnectionStringSetting = "SqlConnectionString")] out ProductColumnTypes product) + { + product = new ProductColumnTypes() + { + ProductID = int.Parse(req.Query["productId"]), + Datetime = DateTime.UtcNow, + Datetime2 = DateTime.UtcNow + }; + + // Items were inserted successfully so return success, an exception would be thrown if there + // was any issues + return new OkObjectResult("Success!"); + } + } +} diff --git a/test/Integration/test-csharp/GetProductColumnTypesSerialization.cs b/test/Integration/test-csharp/GetProductColumnTypesSerialization.cs new file mode 100644 index 000000000..836c305ca --- /dev/null +++ b/test/Integration/test-csharp/GetProductColumnTypesSerialization.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Common; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Azure.WebJobs.Extensions.Sql.Samples.InputBindingSamples +{ + public static class GetProductsColumnTypesSerialization + { + /// + /// This function verifies that serializing an item with various data types + /// works as expected. + /// Note this uses IAsyncEnumerable because IEnumerable serializes the entire table directly, + /// instead of each item one by one (which is where issues can occur) + /// + [FunctionName(nameof(GetProductsColumnTypesSerialization))] + public static async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "getproducts-columntypesserialization")] + HttpRequest req, + [Sql("SELECT * FROM [dbo].[ProductsColumnTypes]", + CommandType = System.Data.CommandType.Text, + ConnectionStringSetting = "SqlConnectionString")] + IAsyncEnumerable products, + ILogger log) + { + await foreach (ProductColumnTypes item in products) + { + log.LogInformation(JsonSerializer.Serialize(item)); + } + return new OkObjectResult(products); + } + } +} diff --git a/test/Integration/test-csharp/GetProductColumnTypesSerializationDifferentCulture.cs b/test/Integration/test-csharp/GetProductColumnTypesSerializationDifferentCulture.cs new file mode 100644 index 000000000..9417ba46a --- /dev/null +++ b/test/Integration/test-csharp/GetProductColumnTypesSerializationDifferentCulture.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Globalization; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Common; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Azure.WebJobs.Extensions.Sql.Samples.InputBindingSamples +{ + public static class GetProductsColumnTypesSerializationDifferentCulture + { + /// + /// This function verifies that serializing an item with various data types + /// works when the language is set to a non-enUS language. + /// Note this uses IAsyncEnumerable because IEnumerable serializes the entire table directly, + /// instead of each item one by one (which is where issues can occur) + /// + [FunctionName(nameof(GetProductsColumnTypesSerializationDifferentCulture))] + public static async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "getproducts-columntypesserializationdifferentculture")] + HttpRequest req, + [Sql("SELECT * FROM [dbo].[ProductsColumnTypes]", + CommandType = System.Data.CommandType.Text, + ConnectionStringSetting = "SqlConnectionString")] + IAsyncEnumerable products, + ILogger log) + { + CultureInfo.CurrentCulture = new CultureInfo("it-IT", false); + await foreach (ProductColumnTypes item in products) + { + log.LogInformation(JsonSerializer.Serialize(item)); + } + return new OkObjectResult(products); + } + } +} diff --git a/test/Integration/test-csharp/MultiFunctionTrigger.cs b/test/Integration/test-csharp/MultiFunctionTrigger.cs new file mode 100644 index 000000000..66d25b267 --- /dev/null +++ b/test/Integration/test-csharp/MultiFunctionTrigger.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. 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.WebJobs.Extensions.Sql.Samples.Common; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Integration +{ + /// + /// Used to ensure correct functionality with multiple user functions tracking the same table. + /// + public static class MultiFunctionTrigger + { + [FunctionName(nameof(MultiFunctionTrigger1))] + public static void MultiFunctionTrigger1( + [SqlTrigger("[dbo].[Products]", ConnectionStringSetting = "SqlConnectionString")] + IReadOnlyList> products, + ILogger logger) + { + logger.LogInformation("Trigger1 Changes: " + JsonConvert.SerializeObject(products)); + } + + [FunctionName(nameof(MultiFunctionTrigger2))] + public static void MultiFunctionTrigger2( + [SqlTrigger("[dbo].[Products]", ConnectionStringSetting = "SqlConnectionString")] + IReadOnlyList> products, + ILogger logger) + { + logger.LogInformation("Trigger2 Changes: " + JsonConvert.SerializeObject(products)); + } + } +} diff --git a/test/Integration/test-csharp/ProductsTriggerWithValidation.cs b/test/Integration/test-csharp/ProductsTriggerWithValidation.cs new file mode 100644 index 000000000..7fca5cbf9 --- /dev/null +++ b/test/Integration/test-csharp/ProductsTriggerWithValidation.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. 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 Microsoft.Azure.WebJobs.Extensions.Sql.Samples.Common; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Integration +{ + public static class ProductsTriggerWithValidation + { + /// + /// Simple trigger function with additional logic to allow for verifying that the expected number + /// of changes was recieved in each batch. + /// + [FunctionName(nameof(ProductsTriggerWithValidation))] + public static void Run( + [SqlTrigger("[dbo].[Products]", ConnectionStringSetting = "SqlConnectionString")] + IReadOnlyList> changes, + ILogger logger) + { + string expectedBatchSize = Environment.GetEnvironmentVariable("TEST_EXPECTED_BATCH_SIZE"); + if (!string.IsNullOrEmpty(expectedBatchSize) && int.Parse(expectedBatchSize) != changes.Count) + { + throw new Exception($"Invalid batch size, got {changes.Count} changes but expected {expectedBatchSize}"); + } + // The output is used to inspect the trigger binding parameter in test methods. + logger.LogInformation("SQL Changes: " + JsonConvert.SerializeObject(changes)); + } + } +} diff --git a/test/Integration/test-js/AddProductColumnTypes/function.json b/test/Integration/test-js/AddProductColumnTypes/function.json new file mode 100644 index 000000000..3b69ac6ae --- /dev/null +++ b/test/Integration/test-js/AddProductColumnTypes/function.json @@ -0,0 +1,27 @@ +{ + "bindings": [ + { + "authLevel": "function", + "name": "req", + "direction": "in", + "type": "httpTrigger", + "methods": [ + "get" + ], + "route": "addproduct-columntypes" + }, + { + "name": "$return", + "type": "http", + "direction": "out" + }, + { + "name": "product", + "type": "sql", + "direction": "out", + "commandText": "[dbo].[ProductsColumnTypes]", + "connectionStringSetting": "SqlConnectionString" + } + ], + "disabled": false +} \ No newline at end of file diff --git a/test/Integration/test-js/AddProductColumnTypes/index.js b/test/Integration/test-js/AddProductColumnTypes/index.js new file mode 100644 index 000000000..8d83ac300 --- /dev/null +++ b/test/Integration/test-js/AddProductColumnTypes/index.js @@ -0,0 +1,18 @@ +/** + * This function is used to test compatability with converting various data types to their respective + * SQL server types. + */ +module.exports = async function (context, req) { + const product = { + "productId": req.query.productId, + "datetime": Date.now(), + "datetime2": Date.now() + }; + + context.bindings.product = JSON.stringify(product); + + return { + status: 201, + body: product + }; +} \ No newline at end of file diff --git a/test/Integration/test-js/GetProductsColumnTypesSerialization/function.json b/test/Integration/test-js/GetProductsColumnTypesSerialization/function.json new file mode 100644 index 000000000..0554a9f7d --- /dev/null +++ b/test/Integration/test-js/GetProductsColumnTypesSerialization/function.json @@ -0,0 +1,28 @@ +{ + "bindings": [ + { + "authLevel": "function", + "name": "req", + "type": "httpTrigger", + "direction": "in", + "methods": [ + "get" + ], + "route": "getproducts-columntypesserialization" + }, + { + "name": "$return", + "type": "http", + "direction": "out" + }, + { + "name": "products", + "type": "sql", + "direction": "in", + "commandText": "SELECT * FROM [dbo].[ProductsColumnTypes]", + "commandType": "Text", + "connectionStringSetting": "SqlConnectionString" + } + ], + "disabled": false +} \ No newline at end of file diff --git a/test/Integration/test-js/GetProductsColumnTypesSerialization/index.js b/test/Integration/test-js/GetProductsColumnTypesSerialization/index.js new file mode 100644 index 000000000..732f2c8af --- /dev/null +++ b/test/Integration/test-js/GetProductsColumnTypesSerialization/index.js @@ -0,0 +1,11 @@ +/** + * This function verifies that serializing an item with various data types + * works as expected. + */ +module.exports = async function (context, req, products) { + context.log(JSON.stringify(products)); + return { + status: 200, + body: products + }; +} \ No newline at end of file diff --git a/test/Microsoft.Azure.WebJobs.Extensions.Sql.Tests.csproj b/test/Microsoft.Azure.WebJobs.Extensions.Sql.Tests.csproj index 54c504783..cad70ffbc 100644 --- a/test/Microsoft.Azure.WebJobs.Extensions.Sql.Tests.csproj +++ b/test/Microsoft.Azure.WebJobs.Extensions.Sql.Tests.csproj @@ -15,8 +15,14 @@ - - + + + + + + + Always + @@ -32,13 +38,4 @@ - - - - <_CSharpTestSqlFiles Include="Integration\test-csharp\Database\**\*.*" /> - - - - - diff --git a/test/README.md b/test/README.md index 1824c49c2..7be921e56 100644 --- a/test/README.md +++ b/test/README.md @@ -14,18 +14,24 @@ Our integration tests are based on functions from the samples project. To run in ``` npm install -g azurite ``` -3. A local SQL Server instance - This is used by tests to verify that data is correctly added/fetched from the database when a test Function is run. You just need the server to be up and running, the tests will create the database and tables which will be cleaned up afterwards. +3. A SQL Server instance - This is used by tests to verify that data is correctly added/fetched from the database when a test Function is run. You just need the server to be up and running, the tests will create the database and tables which will be cleaned up afterwards. - - You can either have a SQL Server installation with `localhost` available for connection via integrated security, or - - Start a SQL Server instance with Docker + ### Local Install + To use a SQL Server installation, ensure `localhost` is available for connection via integrated security. + + ### Docker Container + Start a SQL Server instance with Docker ``` docker pull mcr.microsoft.com/mssql/server:2019-latest docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD={your_password}" -e "MSSQL_PID=Express" -p 1433:1433 --name sql1 -h sql1 -d mcr.microsoft.com/mssql/server:2019-latest ``` After the Docker image is running, you just need to set `SA_PASSWORD` environment variable to `{your_password}` and can run tests normally. - + Note: If `SA_PASSWORD` is not set, the tests will assume you're using a local MSSQL installation and default to using integrated auth. MSSQL on Docker does not support integrated auth by default. + ### Azure SQL Database + To use an Azure SQL Database, set the `TEST_CONNECTION_STRING` environment variable to your Azure SQL Database connection string. + ## Adding New Integration Tests When adding a new integration test for a function follow these steps: @@ -35,7 +41,7 @@ Our integration tests are based on functions from the samples project. To run in 4. After the functions are created then add the test itself to either SqlInputBindingIntegrationTests.ts or SqlOutputBindingIntegrationTests.ts. See below for the various attributes, parameters and setup that are required for each test ### SqlInlineData attribute: - + SqlInlineData attribute is a custom attribute derived from Xunit Data attribute and it supplies the SupportedLanguage parameter to the test for the test to run against in addition to any other data parameters included. By default any test decorated with the [SqlInlineData] attribute will be run against each supported language in the SupportedLanguages enum. How to use: Add [Theory] and [SqlInlineData] attributes over the test and pass in the test variables except the language variable. diff --git a/test/Unit/SqlInputBindingTests.cs b/test/Unit/SqlInputBindingTests.cs index 0e6701dc5..8da337d07 100644 --- a/test/Unit/SqlInputBindingTests.cs +++ b/test/Unit/SqlInputBindingTests.cs @@ -13,6 +13,7 @@ using Moq; using Xunit; using Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Common; +using Microsoft.Azure.WebJobs.Extensions.Sql.Telemetry; namespace Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Unit { @@ -30,7 +31,7 @@ public void TestNullConfiguration() Assert.Throws(() => new SqlBindingConfigProvider(null, hostIdProvider.Object, loggerFactory.Object)); Assert.Throws(() => new SqlBindingConfigProvider(config.Object, null, loggerFactory.Object)); Assert.Throws(() => new SqlBindingConfigProvider(config.Object, hostIdProvider.Object, null)); - Assert.Throws(() => new SqlConverter(null)); + Assert.Throws(() => new SqlConverter(null, logger.Object)); Assert.Throws(() => new SqlGenericsConverter(null, logger.Object)); } @@ -63,17 +64,18 @@ public void TestNullCommand() [Fact] public void TestNullArgumentsSqlAsyncEnumerableConstructor() { - Assert.Throws(() => new SqlAsyncEnumerable(connection, null)); Assert.Throws(() => new SqlAsyncEnumerable(null, new SqlAttribute(""))); } + /// + /// SqlAsyncEnumerable should throw InvalidOperationExcepion when invoked with an invalid connection + /// string setting and It should fail here since we're passing an empty connection string. + /// [Fact] - public void TestNullCurrentValueEnumerator() + public void TestInvalidOperationSqlAsyncEnumerableConstructor() { - var enumerable = new SqlAsyncEnumerable(connection, new SqlAttribute("")); - IAsyncEnumerator enumerator = enumerable.GetAsyncEnumerator(); - Assert.Null(enumerator.Current); + Assert.Throws(() => new SqlAsyncEnumerable(connection, new SqlAttribute(""))); } [Fact] @@ -233,7 +235,7 @@ public async void TestWellformedDeserialization() var converter = new Mock>(config.Object, logger.Object); string json = "[{ \"ID\":1,\"Name\":\"Broom\",\"Cost\":32.5,\"Timestamp\":\"2019-11-22T06:32:15\"},{ \"ID\":2,\"Name\":\"Brush\",\"Cost\":12.3," + "\"Timestamp\":\"2017-01-27T03:13:11\"},{ \"ID\":3,\"Name\":\"Comb\",\"Cost\":100.12,\"Timestamp\":\"1997-05-03T10:11:56\"}]"; - converter.Setup(_ => _.BuildItemFromAttributeAsync(arg)).ReturnsAsync(json); + converter.Setup(_ => _.BuildItemFromAttributeAsync(arg, ConvertType.IEnumerable)).ReturnsAsync(json); var list = new List(); var data1 = new TestData { @@ -271,7 +273,7 @@ public async void TestMalformedDeserialization() // SQL data is missing a field string json = "[{ \"ID\":1,\"Name\":\"Broom\",\"Timestamp\":\"2019-11-22T06:32:15\"}]"; - converter.Setup(_ => _.BuildItemFromAttributeAsync(arg)).ReturnsAsync(json); + converter.Setup(_ => _.BuildItemFromAttributeAsync(arg, ConvertType.IEnumerable)).ReturnsAsync(json); var list = new List(); var data = new TestData { @@ -286,7 +288,7 @@ public async void TestMalformedDeserialization() // SQL data's columns are named differently than the POCO's fields json = "[{ \"ID\":1,\"Product Name\":\"Broom\",\"Price\":32.5,\"Timessstamp\":\"2019-11-22T06:32:15\"}]"; - converter.Setup(_ => _.BuildItemFromAttributeAsync(arg)).ReturnsAsync(json); + converter.Setup(_ => _.BuildItemFromAttributeAsync(arg, ConvertType.IEnumerable)).ReturnsAsync(json); list = new List(); data = new TestData { @@ -300,7 +302,7 @@ public async void TestMalformedDeserialization() // Confirm that the JSON fields are case-insensitive (technically malformed string, but still works) json = "[{ \"id\":1,\"nAme\":\"Broom\",\"coSt\":32.5,\"TimEStamp\":\"2019-11-22T06:32:15\"}]"; - converter.Setup(_ => _.BuildItemFromAttributeAsync(arg)).ReturnsAsync(json); + converter.Setup(_ => _.BuildItemFromAttributeAsync(arg, ConvertType.IEnumerable)).ReturnsAsync(json); list = new List(); data = new TestData { diff --git a/test/packages.lock.json b/test/packages.lock.json index 65443dcee..8a77ec072 100644 --- a/test/packages.lock.json +++ b/test/packages.lock.json @@ -1930,23 +1930,23 @@ "microsoft.azure.webjobs.extensions.sql": { "type": "Project", "dependencies": { - "Microsoft.ApplicationInsights": "2.14.0", - "Microsoft.AspNetCore.Http": "2.1.22", - "Microsoft.Azure.WebJobs": "3.0.31", - "Microsoft.Data.SqlClient": "3.0.1", - "Newtonsoft.Json": "11.0.2", - "System.Runtime.Caching": "4.7.0", - "morelinq": "3.3.2" + "Microsoft.ApplicationInsights": "[2.14.0, )", + "Microsoft.AspNetCore.Http": "[2.1.22, )", + "Microsoft.Azure.WebJobs": "[3.0.31, )", + "Microsoft.Data.SqlClient": "[3.0.1, )", + "Newtonsoft.Json": "[11.0.2, )", + "System.Runtime.Caching": "[4.7.0, )", + "morelinq": "[3.3.2, )" } }, "microsoft.azure.webjobs.extensions.sql.samples": { "type": "Project", "dependencies": { - "Microsoft.AspNetCore.Http": "2.1.22", - "Microsoft.Azure.WebJobs.Extensions.Sql": "99.99.99", - "Microsoft.Azure.WebJobs.Extensions.Storage": "5.0.0", - "Microsoft.NET.Sdk.Functions": "3.1.1", - "Newtonsoft.Json": "11.0.2" + "Microsoft.AspNetCore.Http": "[2.1.22, )", + "Microsoft.Azure.WebJobs.Extensions.Sql": "[99.99.99, )", + "Microsoft.Azure.WebJobs.Extensions.Storage": "[5.0.0, )", + "Microsoft.NET.Sdk.Functions": "[3.1.1, )", + "Newtonsoft.Json": "[11.0.2, )" } }, "Microsoft.ApplicationInsights": {