Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions samples/samples-js/ProductsTrigger/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"bindings": [
{
"name": "changes",
"type": "sqlTrigger",
"direction": "in",
"tableName": "Products",
"connectionStringSetting": "SqlConnectionString"
}
],
"disabled": false
}
6 changes: 6 additions & 0 deletions samples/samples-js/ProductsTrigger/index.js
Original file line number Diff line number Diff line change
@@ -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)}`)
}
12 changes: 12 additions & 0 deletions samples/samples-powershell/ProductsTrigger/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"bindings": [
{
"name": "changes",
"type": "sqlTrigger",
"direction": "in",
"tableName": "Products",
"connectionStringSetting": "SqlConnectionString"
}
],
"disabled": false
}
11 changes: 11 additions & 0 deletions samples/samples-powershell/ProductsTrigger/run.ps1
Original file line number Diff line number Diff line change
@@ -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"
8 changes: 8 additions & 0 deletions samples/samples-python/ProductsTrigger/__init__.py
Original file line number Diff line number Diff line change
@@ -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))
12 changes: 12 additions & 0 deletions samples/samples-python/ProductsTrigger/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"bindings": [
{
"name": "changes",
"type": "sqlTrigger",
"direction": "in",
"tableName": "Products",
"connectionStringSetting": "SqlConnectionString"
}
],
"disabled": false
}
30 changes: 20 additions & 10 deletions src/TriggerBinding/SqlTriggerBindingProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -65,18 +66,26 @@ public Task<ITriggerBinding> TryCreateAsync(TriggerBindingProviderContext contex
return Task.FromResult<ITriggerBinding>(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<SqlChange<T>> 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<T>'.
Type userType = parameter.ParameterType.GetGenericArguments()[0].GetGenericArguments()[0];
Type bindingType = typeof(SqlTriggerBinding<>).MakeGenericType(userType);
Type bindingType;
// Instantiate class 'SqlTriggerBinding<JObject>' 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<T>'.
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);
Expand All @@ -88,15 +97,16 @@ public Task<ITriggerBinding> TryCreateAsync(TriggerBindingProviderContext contex
}

/// <summary>
/// Checks if the type of trigger parameter in the user function is of form <see cref="IReadOnlyList<SqlChange{T}>" />.
/// Checks if the type of trigger parameter in the user function is of form string or <see cref="IReadOnlyList<SqlChange{T}>" />.
/// </summary>
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<>));
}
}
}
25 changes: 22 additions & 3 deletions src/TriggerBinding/SqlTriggerValueProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -15,6 +16,8 @@ internal class SqlTriggerValueProvider : IValueProvider
{
private readonly object _value;
private readonly string _tableName;
private readonly Type _parameterType;
private readonly bool _isString;

/// <summary>
/// Initializes a new instance of the <see cref="SqlTriggerValueProvider"/> class.
Expand All @@ -24,21 +27,37 @@ internal class SqlTriggerValueProvider : IValueProvider
/// <param name="tableName">Name of the user table</param>
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);
}

/// <summary>
/// Gets the trigger argument value.
/// </summary>
public Type Type { get; }
public Type Type
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Followed the CosmosDB extension implementation here. They also support JArray but the languages I tested so far (JS, PS, Python) are only using String. We can add JArray later if needed for other languages/scenarios.

{
get
{
if (this._isString)
{
return typeof(IReadOnlyCollection<JObject>);
}
return this._parameterType;
}
}

/// <summary>
/// Returns value of the trigger argument.
/// </summary>
public Task<object> GetValueAsync()
{
if (this._isString)
{
return Task.FromResult<object>(Utils.JsonSerializeObject(this._value));
}

return Task.FromResult(this._value);
}

Expand Down
8 changes: 5 additions & 3 deletions test/Integration/SqlTriggerBindingIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ public SqlTriggerBindingIntegrationTests(ITestOutputHelper output = null) : base
/// <summary>
/// Ensures that the user function gets invoked for each of the insert, update and delete operation.
/// </summary>
[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;
Expand Down
2 changes: 1 addition & 1 deletion test/Unit/TriggerBinding/SqlTriggerBindingProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public async Task TryCreateAsync_InvalidTriggerParameterType_ThrowsException(Typ
InvalidOperationException exception = await Assert.ThrowsAsync<InvalidOperationException>(testCode);

Assert.Equal(
$"Can't bind SqlTriggerAttribute to type {parameterType}. Only IReadOnlyList<SqlChange<T>> 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);
}

Expand Down