diff --git a/samples/samples-js/ProductsTrigger/function.json b/samples/samples-js/ProductsTrigger/function.json new file mode 100644 index 000000000..3bc5a8141 --- /dev/null +++ b/samples/samples-js/ProductsTrigger/function.json @@ -0,0 +1,12 @@ +{ + "bindings": [ + { + "name": "changes", + "type": "sqlTrigger", + "direction": "in", + "tableName": "Products", + "connectionStringSetting": "SqlConnectionString" + } + ], + "disabled": false + } \ No newline at end of file diff --git a/samples/samples-js/ProductsTrigger/index.js b/samples/samples-js/ProductsTrigger/index.js new file mode 100644 index 000000000..5886e5c82 --- /dev/null +++ b/samples/samples-js/ProductsTrigger/index.js @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +module.exports = async function (context, changes) { + context.log(`SQL Changes: ${JSON.stringify(changes)}`) +} \ No newline at end of file diff --git a/samples/samples-powershell/ProductsTrigger/function.json b/samples/samples-powershell/ProductsTrigger/function.json new file mode 100644 index 000000000..3bc5a8141 --- /dev/null +++ b/samples/samples-powershell/ProductsTrigger/function.json @@ -0,0 +1,12 @@ +{ + "bindings": [ + { + "name": "changes", + "type": "sqlTrigger", + "direction": "in", + "tableName": "Products", + "connectionStringSetting": "SqlConnectionString" + } + ], + "disabled": false + } \ No newline at end of file diff --git a/samples/samples-powershell/ProductsTrigger/run.ps1 b/samples/samples-powershell/ProductsTrigger/run.ps1 new file mode 100644 index 000000000..74f920178 --- /dev/null +++ b/samples/samples-powershell/ProductsTrigger/run.ps1 @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. + +using namespace System.Net + +param($changes) +$changesJson = $changes | ConvertTo-Json +# The output is used to inspect the trigger binding parameter in test methods. +# Removing new lines for testing purposes. +$changesJson = $changesJson -replace [Environment]::NewLine,""; +Write-Host "SQL Changes: $changesJson" \ No newline at end of file diff --git a/samples/samples-python/ProductsTrigger/__init__.py b/samples/samples-python/ProductsTrigger/__init__.py new file mode 100644 index 000000000..4ef0f7b40 --- /dev/null +++ b/samples/samples-python/ProductsTrigger/__init__.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import logging + +def main(changes): + logging.info("SQL Changes: %s", json.loads(changes)) diff --git a/samples/samples-python/ProductsTrigger/function.json b/samples/samples-python/ProductsTrigger/function.json new file mode 100644 index 000000000..3bc5a8141 --- /dev/null +++ b/samples/samples-python/ProductsTrigger/function.json @@ -0,0 +1,12 @@ +{ + "bindings": [ + { + "name": "changes", + "type": "sqlTrigger", + "direction": "in", + "tableName": "Products", + "connectionStringSetting": "SqlConnectionString" + } + ], + "disabled": false + } \ No newline at end of file diff --git a/src/TriggerBinding/SqlTriggerBindingProvider.cs b/src/TriggerBinding/SqlTriggerBindingProvider.cs index 17afa4cc3..5640c3251 100644 --- a/src/TriggerBinding/SqlTriggerBindingProvider.cs +++ b/src/TriggerBinding/SqlTriggerBindingProvider.cs @@ -10,6 +10,7 @@ using Microsoft.Azure.WebJobs.Logging; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; namespace Microsoft.Azure.WebJobs.Extensions.Sql { @@ -65,18 +66,26 @@ public Task TryCreateAsync(TriggerBindingProviderContext contex return Task.FromResult(null); } - if (!IsValidTriggerParameterType(parameter.ParameterType)) + Type parameterType = parameter.ParameterType; + if (!IsValidTriggerParameterType(parameterType)) { - throw new InvalidOperationException($"Can't bind SqlTriggerAttribute to type {parameter.ParameterType}." + - " Only IReadOnlyList> is supported, where T is the type of user-defined POCO that" + - " matches the schema of the user table"); + throw new InvalidOperationException($"Can't bind SqlTriggerAttribute to type {parameter.ParameterType}, this is not a supported type."); } string connectionString = SqlBindingUtilities.GetConnectionString(attribute.ConnectionStringSetting, this._configuration); - // Extract the POCO type 'T' and use it to instantiate class 'SqlTriggerBinding'. - Type userType = parameter.ParameterType.GetGenericArguments()[0].GetGenericArguments()[0]; - Type bindingType = typeof(SqlTriggerBinding<>).MakeGenericType(userType); + Type bindingType; + // Instantiate class 'SqlTriggerBinding' for non .NET In-Proc functions. + if (parameterType == typeof(string)) + { + bindingType = typeof(SqlTriggerBinding<>).MakeGenericType(typeof(JObject)); + } + else + { + // Extract the POCO type 'T' and use it to instantiate class 'SqlTriggerBinding'. + Type userType = parameter.ParameterType.GetGenericArguments()[0].GetGenericArguments()[0]; + bindingType = typeof(SqlTriggerBinding<>).MakeGenericType(userType); + } var constructorParameterTypes = new Type[] { typeof(string), typeof(string), typeof(ParameterInfo), typeof(IHostIdProvider), typeof(ILogger), typeof(IConfiguration) }; ConstructorInfo bindingConstructor = bindingType.GetConstructor(constructorParameterTypes); @@ -88,15 +97,16 @@ public Task TryCreateAsync(TriggerBindingProviderContext contex } /// - /// Checks if the type of trigger parameter in the user function is of form . + /// Checks if the type of trigger parameter in the user function is of form string or . /// private static bool IsValidTriggerParameterType(Type type) { return - type.IsGenericType && + type == typeof(string) || + (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IReadOnlyList<>) && type.GetGenericArguments()[0].IsGenericType && - type.GetGenericArguments()[0].GetGenericTypeDefinition() == typeof(SqlChange<>); + type.GetGenericArguments()[0].GetGenericTypeDefinition() == typeof(SqlChange<>)); } } } \ No newline at end of file diff --git a/src/TriggerBinding/SqlTriggerValueProvider.cs b/src/TriggerBinding/SqlTriggerValueProvider.cs index db1d774ac..50db92364 100644 --- a/src/TriggerBinding/SqlTriggerValueProvider.cs +++ b/src/TriggerBinding/SqlTriggerValueProvider.cs @@ -2,9 +2,10 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Azure.WebJobs.Host.Bindings; - +using Newtonsoft.Json.Linq; namespace Microsoft.Azure.WebJobs.Extensions.Sql { @@ -15,6 +16,8 @@ internal class SqlTriggerValueProvider : IValueProvider { private readonly object _value; private readonly string _tableName; + private readonly Type _parameterType; + private readonly bool _isString; /// /// Initializes a new instance of the class. @@ -24,21 +27,37 @@ internal class SqlTriggerValueProvider : IValueProvider /// Name of the user table public SqlTriggerValueProvider(Type parameterType, object value, string tableName) { - this.Type = parameterType; + this._parameterType = parameterType; this._value = value; this._tableName = tableName; + this._isString = parameterType == typeof(string); } /// /// Gets the trigger argument value. /// - public Type Type { get; } + public Type Type + { + get + { + if (this._isString) + { + return typeof(IReadOnlyCollection); + } + return this._parameterType; + } + } /// /// Returns value of the trigger argument. /// public Task GetValueAsync() { + if (this._isString) + { + return Task.FromResult(Utils.JsonSerializeObject(this._value)); + } + return Task.FromResult(this._value); } diff --git a/test/Integration/SqlTriggerBindingIntegrationTests.cs b/test/Integration/SqlTriggerBindingIntegrationTests.cs index 06cf487c9..e33bdefd3 100644 --- a/test/Integration/SqlTriggerBindingIntegrationTests.cs +++ b/test/Integration/SqlTriggerBindingIntegrationTests.cs @@ -29,11 +29,13 @@ public SqlTriggerBindingIntegrationTests(ITestOutputHelper output = null) : base /// /// Ensures that the user function gets invoked for each of the insert, update and delete operation. /// - [Fact] - public async Task SingleOperationTriggerTest() + [Theory] + [SqlInlineData()] + [UnsupportedLanguages(SupportedLanguages.Java, SupportedLanguages.OutOfProc)] + public async Task SingleOperationTriggerTest(SupportedLanguages lang) { this.SetChangeTrackingForTable("Products"); - this.StartFunctionHost(nameof(ProductsTrigger), SupportedLanguages.CSharp); + this.StartFunctionHost(nameof(ProductsTrigger), lang); int firstId = 1; int lastId = 30; diff --git a/test/Unit/TriggerBinding/SqlTriggerBindingProviderTests.cs b/test/Unit/TriggerBinding/SqlTriggerBindingProviderTests.cs index 2db09855d..85c6b5cf9 100644 --- a/test/Unit/TriggerBinding/SqlTriggerBindingProviderTests.cs +++ b/test/Unit/TriggerBinding/SqlTriggerBindingProviderTests.cs @@ -62,7 +62,7 @@ public async Task TryCreateAsync_InvalidTriggerParameterType_ThrowsException(Typ InvalidOperationException exception = await Assert.ThrowsAsync(testCode); Assert.Equal( - $"Can't bind SqlTriggerAttribute to type {parameterType}. Only IReadOnlyList> is supported, where T is the type of user-defined POCO that matches the schema of the user table", + $"Can't bind SqlTriggerAttribute to type {parameterType}, this is not a supported type.", exception.Message); }