Insert sql bindings into Azure Functions (#1224)

* getting table name from a script

* add InsertSqlInputBindingOperation

* cleanup

* move azure functions stuff out of dacfx service

* cleanup

* add tests

* add another test

* cleanup

* add comments and connection string setting

* addressing comments

* change name to use add instead of insert
This commit is contained in:
Kim Santiago
2021-08-04 13:02:52 -07:00
committed by GitHub
parent a7703e63a4
commit b1653b25e4
15 changed files with 682 additions and 1 deletions

View File

@@ -26,6 +26,9 @@
<PackageReference Update="Microsoft.SqlServer.Assessment" Version="[1.0.305]" />
<PackageReference Update="Microsoft.SqlServer.Migration.Assessment" Version="1.0.20210714.5" />
<PackageReference Update="Microsoft.Azure.OperationalInsights" Version="1.0.0" />
<PackageReference Update="Microsoft.CodeAnalysis.CSharp" Version="3.10.0" />
<PackageReference Update="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.10.0" />
<PackageReference Update="Microsoft.CodeAnalysis.Workspaces.Common" Version="3.10.0" />
<PackageReference Update="Moq" Version="4.8.2" />
<PackageReference Update="nunit" Version="3.12.0" />

View File

@@ -0,0 +1,143 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.SqlTools.ServiceLayer.AzureFunctions.Contracts;
using Microsoft.SqlTools.ServiceLayer.Utility;
using Microsoft.SqlTools.Utility;
using Microsoft.CodeAnalysis.CSharp;
namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions
{
/// <summary>
/// Class to represent inserting a sql binding into an Azure Function
/// </summary>
class AddSqlBindingOperation
{
const string functionAttributeText = "FunctionName";
public AddSqlBindingParams Parameters { get; }
public AddSqlBindingOperation(AddSqlBindingParams parameters)
{
Validate.IsNotNull("parameters", parameters);
this.Parameters = parameters;
}
public ResultStatus AddBinding()
{
try
{
string text = File.ReadAllText(Parameters.filePath);
SyntaxTree tree = CSharpSyntaxTree.ParseText(text);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();
// look for Azure Function to update
IEnumerable<MethodDeclarationSyntax> azureFunctionMethods = from methodDeclaration in root.DescendantNodes().OfType<MethodDeclarationSyntax>()
where methodDeclaration.AttributeLists.Count > 0
where methodDeclaration.AttributeLists.Where(a => a.Attributes.Where(attr => attr.Name.ToString().Contains(functionAttributeText) && attr.ArgumentList.Arguments.First().ToString().Equals($"\"{Parameters.functionName}\"")).Count() == 1).Count() == 1
select methodDeclaration;
if (azureFunctionMethods.Count() == 0)
{
return new ResultStatus()
{
Success = false,
ErrorMessage = SR.CouldntFindAzureFunction(Parameters.functionName, Parameters.filePath)
};
}
else if (azureFunctionMethods.Count() > 1)
{
return new ResultStatus()
{
Success = false,
ErrorMessage = SR.MoreThanOneAzureFunctionWithName(Parameters.functionName, Parameters.filePath)
};
}
MethodDeclarationSyntax azureFunction = azureFunctionMethods.First();
var newParam = this.Parameters.bindingType == BindingType.input ? this.GenerateInputBinding() : this.GenerateOutputBinding();
// Generate updated method with the new parameter
// normalizewhitespace gets rid of any newline whitespace in the leading trivia, so we add that back
var updatedMethod = azureFunction.AddParameterListParameters(newParam).NormalizeWhitespace().WithLeadingTrivia(azureFunction.GetLeadingTrivia()).WithTrailingTrivia(azureFunction.GetTrailingTrivia());
// Replace the node in the tree
root = root.ReplaceNode(azureFunction, updatedMethod);
// write updated tree to file
var workspace = new AdhocWorkspace();
var syntaxTree = CSharpSyntaxTree.ParseText(root.ToString());
var formattedNode = CodeAnalysis.Formatting.Formatter.Format(syntaxTree.GetRoot(), workspace);
StringBuilder sb = new StringBuilder(formattedNode.ToString());
string content = sb.ToString();
File.WriteAllText(Parameters.filePath, content);
return new ResultStatus()
{
Success = true
};
}
catch (Exception e)
{
return new ResultStatus()
{
Success = false,
ErrorMessage = e.ToString()
};
}
}
/// <summary>
/// Generates a parameter for the sql input binding that looks like
/// [Sql("select * from [dbo].[table1]", CommandType = System.Data.CommandType.Text, ConnectionStringSetting = "SqlConnectionString")] IEnumerable<Object> result
/// </summary>
private ParameterSyntax GenerateInputBinding()
{
// Create arguments for the Sql Input Binding attribute
var argumentList = SyntaxFactory.AttributeArgumentList();
argumentList = argumentList.AddArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.IdentifierName($"\"select * from {Parameters.objectName}\"")));
argumentList = argumentList.AddArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, SyntaxFactory.IdentifierName("CommandType"), SyntaxFactory.IdentifierName("System.Data.CommandType.Text"))));
argumentList = argumentList.AddArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, SyntaxFactory.IdentifierName("ConnectionStringSetting"), SyntaxFactory.IdentifierName($"\"{Parameters.connectionStringSetting}\""))));
// Create Sql Binding attribute
SyntaxList<AttributeListSyntax> attributesList = new SyntaxList<AttributeListSyntax>();
attributesList = attributesList.Add(SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList<AttributeSyntax>(SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("Sql")).WithArgumentList(argumentList))));
// Create new parameter
ParameterSyntax newParam = SyntaxFactory.Parameter(attributesList, new SyntaxTokenList(), SyntaxFactory.ParseTypeName("IEnumerable<Object>"), SyntaxFactory.Identifier("result"), null);
return newParam;
}
/// <summary>
/// Generates a parameter for the sql output binding that looks like
/// [Sql("[dbo].[table1]", ConnectionStringSetting = "SqlConnectionString")] out Object output
/// </summary>
private ParameterSyntax GenerateOutputBinding()
{
// Create arguments for the Sql Output Binding attribute
var argumentList = SyntaxFactory.AttributeArgumentList();
argumentList = argumentList.AddArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.IdentifierName($"\"{Parameters.objectName}\"")));
argumentList = argumentList.AddArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, SyntaxFactory.IdentifierName("ConnectionStringSetting"), SyntaxFactory.IdentifierName($"\"{Parameters.connectionStringSetting}\""))));
SyntaxList<AttributeListSyntax> attributesList = new SyntaxList<AttributeListSyntax>();
attributesList = attributesList.Add(SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList<AttributeSyntax>(SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("Sql")).WithArgumentList(argumentList))));
var syntaxTokenList = new SyntaxTokenList();
syntaxTokenList = syntaxTokenList.Add(SyntaxFactory.Token(SyntaxKind.OutKeyword));
ParameterSyntax newParam = SyntaxFactory.Parameter(attributesList, syntaxTokenList, SyntaxFactory.ParseTypeName(typeof(Object).Name), SyntaxFactory.Identifier("output"), null);
return newParam;
}
}
}

View File

@@ -0,0 +1,56 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Threading.Tasks;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.AzureFunctions.Contracts;
using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions
{
/// <summary>
/// Main class for Azure Functions service
/// </summary>
class AzureFunctionsService
{
private static readonly Lazy<AzureFunctionsService> instance = new Lazy<AzureFunctionsService>(() => new AzureFunctionsService());
/// <summary>
/// Gets the singleton instance object
/// </summary>
public static AzureFunctionsService Instance
{
get { return instance.Value; }
}
/// <summary>
/// Initializes the service instance
/// </summary>
/// <param name="serviceHost"></param>
public void InitializeService(ServiceHost serviceHost)
{
serviceHost.SetRequestHandler(AddSqlBindingRequest.Type, this.HandleAddSqlBindingRequest);
}
/// <summary>
/// Handles request to add sql binding into Azure Functions
/// </summary>
public async Task HandleAddSqlBindingRequest(AddSqlBindingParams parameters, RequestContext<ResultStatus> requestContext)
{
try
{
AddSqlBindingOperation operation = new AddSqlBindingOperation(parameters);
ResultStatus result = operation.AddBinding();
await requestContext.SendResult(result);
}
catch (Exception e)
{
await requestContext.SendError(e);
}
}
}
}

View File

@@ -0,0 +1,60 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Collections.Generic;
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.SchemaCompare.Contracts;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions.Contracts
{
/// <summary>
/// Binding types for sql bindings for Azure Functions
/// </summary>
public enum BindingType
{
input,
output
}
/// <summary>
/// Parameters for adding a sql binding
/// </summary>
public class AddSqlBindingParams
{
/// <summary>
/// Gets or sets the filePath
/// </summary>
public string filePath { get; set; }
/// <summary>
/// Gets or sets the binding type
/// </summary>
public BindingType bindingType { get; set; }
/// <summary>
/// Gets or sets the function name
/// </summary>
public string functionName { get; set; }
/// <summary>
/// Gets or sets the object name
/// </summary>
public string objectName { get; set; }
/// <summary>
/// Gets or sets the connection string setting
/// </summary>
public string connectionStringSetting { get; set; }
}
/// <summary>
/// Defines the Add Sql Binding request
/// </summary>
class AddSqlBindingRequest
{
public static readonly RequestType<AddSqlBindingParams, ResultStatus> Type =
RequestType<AddSqlBindingParams, ResultStatus>.Create("azureFunctions/sqlBinding");
}
}

View File

@@ -10,6 +10,7 @@ using Microsoft.SqlTools.Hosting;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Admin;
using Microsoft.SqlTools.ServiceLayer.Agent;
using Microsoft.SqlTools.ServiceLayer.AzureFunctions;
using Microsoft.SqlTools.ServiceLayer.Cms;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.DacFx;
@@ -133,6 +134,9 @@ namespace Microsoft.SqlTools.ServiceLayer
SchemaCompare.SchemaCompareService.Instance.InitializeService(serviceHost);
serviceProvider.RegisterSingleService(SchemaCompareService.Instance);
AzureFunctions.AzureFunctionsService.Instance.InitializeService(serviceHost);
serviceProvider.RegisterSingleService(AzureFunctionsService.Instance);
ServerConfigService.Instance.InitializeService(serviceHost);
serviceProvider.RegisterSingleService(ServerConfigService.Instance);

View File

@@ -3278,6 +3278,16 @@ namespace Microsoft.SqlTools.ServiceLayer
return Keys.GetString(Keys.SqlAssessmentUnsuppoertedEdition, editionCode);
}
public static string CouldntFindAzureFunction(string functionName, string fileName)
{
return Keys.GetString(Keys.CouldntFindAzureFunction, functionName, fileName);
}
public static string MoreThanOneAzureFunctionWithName(string functionName, string fileName)
{
return Keys.GetString(Keys.MoreThanOneAzureFunctionWithName, functionName, fileName);
}
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Keys
{
@@ -4555,6 +4565,12 @@ namespace Microsoft.SqlTools.ServiceLayer
public const string SqlAssessmentUnsuppoertedEdition = "SqlAssessmentUnsuppoertedEdition";
public const string CouldntFindAzureFunction = "CouldntFindAzureFunction";
public const string MoreThanOneAzureFunctionWithName = "MoreThanOneAzureFunctionWithName";
private Keys()
{ }

View File

@@ -1854,4 +1854,14 @@
<comment>.
Parameters: 0 - editionCode (int) </comment>
</data>
<data name="CouldntFindAzureFunction" xml:space="preserve">
<value>Couldn&apos;t find Azure function with FunctionName {0} in {1}</value>
<comment>.
Parameters: 0 - functionName (string), 1 - fileName (string) </comment>
</data>
<data name="MoreThanOneAzureFunctionWithName" xml:space="preserve">
<value>More than one Azure function found with the FunctionName {0} in {1}</value>
<comment>.
Parameters: 0 - functionName (string), 1 - fileName (string) </comment>
</data>
</root>

View File

@@ -848,4 +848,9 @@ SchemaCompareSessionNotFound = Could not find the schema compare session to canc
SqlAssessmentGenerateScriptTaskName = Generate SQL Assessment script
SqlAssessmentQueryInvalidOwnerUri = Not connected to a server
SqlAssessmentConnectingError = Cannot connect to the server
SqlAssessmentUnsuppoertedEdition(int editionCode) = Unsupported engine edition {0}
SqlAssessmentUnsuppoertedEdition(int editionCode) = Unsupported engine edition {0}
############################################################################
# Azure Functions
CouldntFindAzureFunction(string functionName, string fileName) = Couldn't find Azure function with FunctionName '{0}' in {1}
MoreThanOneAzureFunctionWithName(string functionName, string fileName) = More than one Azure function found with the FunctionName '{0}' in {1}

View File

@@ -2161,6 +2161,16 @@
<target state="new">No External Streaming Job creation TSQL found (EXEC sp_create_streaming_job statement).</target>
<note></note>
</trans-unit>
<trans-unit id="CouldntFindAzureFunction">
<source>Couldn't find Azure function with FunctionName {0} in {1}</source>
<target state="new">Couldn't find Azure function with FunctionName {0} in {1}</target>
<note></note>
</trans-unit>
<trans-unit id="MoreThanOneAzureFunctionWithName">
<source>More than one Azure function found with the FunctionName {0} in {1}</source>
<target state="new">More than one Azure function found with the FunctionName {0} in {1}</target>
<note></note>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -21,6 +21,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
<PackageReference Include="Microsoft.SqlServer.DACFx" />
<PackageReference Include="Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider" />
<PackageReference Include="System.Text.Encoding.CodePages" />

View File

@@ -0,0 +1,59 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Company.Namespace.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
namespace Company.Namespace
{
public class ArtistsApi
{
private ILogger<ArtistsApi> _logger;
/// <summary> Initializes a new instance of ArtistsApi. </summary>
/// <param name="logger"> Class logger. </param>
/// <exception cref="ArgumentNullException"> <paramref name="logger"/> is null. </exception>
public ArtistsApi(ILogger<ArtistsApi> logger)
{
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
_logger = logger;
}
/// <summary> Returns a list of artists. </summary>
/// <param name="req"> Raw HTTP Request. </param>
/// <param name="cancellationToken"> The cancellation token provided on Function shutdown. </param>
[FunctionName("GetArtists_get")]
public IActionResult GetArtists([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "artists")] HttpRequest req, [Sql("select * from [dbo].[table1]", CommandType = System.Data.CommandType.Text, ConnectionStringSetting = "SqlConnectionString")] IEnumerable<Object> result)
{
_logger.LogInformation("HTTP trigger function processed a request.");
// TODO: Handle Documented Responses.
// Spec Defines: HTTP 200
// Spec Defines: HTTP 400
throw new NotImplementedException();
}
/// <summary> Lets a user post a new artist. </summary>
/// <param name="body"> The Artist to use. </param>
/// <param name="req"> Raw HTTP Request. </param>
/// <param name="cancellationToken"> The cancellation token provided on Function shutdown. </param>
/// <exception cref="ArgumentNullException"> <paramref name="body"/> is null. </exception>
[FunctionName("NewArtist_post")]
public IActionResult NewArtist([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "artists")] Artist body, HttpRequest req)
{
_logger.LogInformation("HTTP trigger function processed a request.");
// TODO: Handle Documented Responses.
// Spec Defines: HTTP 200
// Spec Defines: HTTP 400
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,59 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Company.Namespace.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
namespace Company.Namespace
{
public class ArtistsApi
{
private ILogger<ArtistsApi> _logger;
/// <summary> Initializes a new instance of ArtistsApi. </summary>
/// <param name="logger"> Class logger. </param>
/// <exception cref="ArgumentNullException"> <paramref name="logger"/> is null. </exception>
public ArtistsApi(ILogger<ArtistsApi> logger)
{
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
_logger = logger;
}
/// <summary> Returns a list of artists. </summary>
/// <param name="req"> Raw HTTP Request. </param>
/// <param name="cancellationToken"> The cancellation token provided on Function shutdown. </param>
[FunctionName("GetArtists_get")]
public IActionResult GetArtists([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "artists")] HttpRequest req)
{
_logger.LogInformation("HTTP trigger function processed a request.");
// TODO: Handle Documented Responses.
// Spec Defines: HTTP 200
// Spec Defines: HTTP 400
throw new NotImplementedException();
}
/// <summary> Lets a user post a new artist. </summary>
/// <param name="body"> The Artist to use. </param>
/// <param name="req"> Raw HTTP Request. </param>
/// <param name="cancellationToken"> The cancellation token provided on Function shutdown. </param>
/// <exception cref="ArgumentNullException"> <paramref name="body"/> is null. </exception>
[FunctionName("GetArtists_get")]
public IActionResult NewArtist([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "artists")] Artist body, HttpRequest req)
{
_logger.LogInformation("HTTP trigger function processed a request.");
// TODO: Handle Documented Responses.
// Spec Defines: HTTP 200
// Spec Defines: HTTP 400
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,59 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Company.Namespace.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
namespace Company.Namespace
{
public class ArtistsApi
{
private ILogger<ArtistsApi> _logger;
/// <summary> Initializes a new instance of ArtistsApi. </summary>
/// <param name="logger"> Class logger. </param>
/// <exception cref="ArgumentNullException"> <paramref name="logger"/> is null. </exception>
public ArtistsApi(ILogger<ArtistsApi> logger)
{
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
_logger = logger;
}
/// <summary> Returns a list of artists. </summary>
/// <param name="req"> Raw HTTP Request. </param>
/// <param name="cancellationToken"> The cancellation token provided on Function shutdown. </param>
[FunctionName("GetArtists_get")]
public IActionResult GetArtists([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "artists")] HttpRequest req)
{
_logger.LogInformation("HTTP trigger function processed a request.");
// TODO: Handle Documented Responses.
// Spec Defines: HTTP 200
// Spec Defines: HTTP 400
throw new NotImplementedException();
}
/// <summary> Lets a user post a new artist. </summary>
/// <param name="body"> The Artist to use. </param>
/// <param name="req"> Raw HTTP Request. </param>
/// <param name="cancellationToken"> The cancellation token provided on Function shutdown. </param>
/// <exception cref="ArgumentNullException"> <paramref name="body"/> is null. </exception>
[FunctionName("NewArtist_post")]
public IActionResult NewArtist([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "artists")] Artist body, HttpRequest req)
{
_logger.LogInformation("HTTP trigger function processed a request.");
// TODO: Handle Documented Responses.
// Spec Defines: HTTP 200
// Spec Defines: HTTP 400
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,59 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Company.Namespace.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
namespace Company.Namespace
{
public class ArtistsApi
{
private ILogger<ArtistsApi> _logger;
/// <summary> Initializes a new instance of ArtistsApi. </summary>
/// <param name="logger"> Class logger. </param>
/// <exception cref="ArgumentNullException"> <paramref name="logger"/> is null. </exception>
public ArtistsApi(ILogger<ArtistsApi> logger)
{
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
_logger = logger;
}
/// <summary> Returns a list of artists. </summary>
/// <param name="req"> Raw HTTP Request. </param>
/// <param name="cancellationToken"> The cancellation token provided on Function shutdown. </param>
[FunctionName("GetArtists_get")]
public IActionResult GetArtists([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "artists")] HttpRequest req)
{
_logger.LogInformation("HTTP trigger function processed a request.");
// TODO: Handle Documented Responses.
// Spec Defines: HTTP 200
// Spec Defines: HTTP 400
throw new NotImplementedException();
}
/// <summary> Lets a user post a new artist. </summary>
/// <param name="body"> The Artist to use. </param>
/// <param name="req"> Raw HTTP Request. </param>
/// <param name="cancellationToken"> The cancellation token provided on Function shutdown. </param>
/// <exception cref="ArgumentNullException"> <paramref name="body"/> is null. </exception>
[FunctionName("NewArtist_post")]
public IActionResult NewArtist([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "artists")] Artist body, HttpRequest req, [Sql("[dbo].[table1]", ConnectionStringSetting = "SqlConnectionString")] out Object output)
{
_logger.LogInformation("HTTP trigger function processed a request.");
// TODO: Handle Documented Responses.
// Spec Defines: HTTP 200
// Spec Defines: HTTP 400
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,135 @@
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.AzureFunctions;
using Microsoft.SqlTools.ServiceLayer.AzureFunctions.Contracts;
using Microsoft.SqlTools.ServiceLayer.Utility;
using Moq;
using NUnit.Framework;
using System;
using System.IO;
using System.Threading.Tasks;
namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.AzureFunctions
{
class AzureFunctionsServiceTests
{
private string testAzureFunctionsFolder = Path.Combine("..", "..", "..", "AzureFunctions", "AzureFunctionTestFiles");
/// <summary>
/// Verify input binding gets added
/// </summary>
[Test]
public void AddSqlInputBinding()
{
// copy the original file because the input binding will be inserted into the file
string originalFile = Path.Join(testAzureFunctionsFolder, "AzureFunctionsNoBindings.ts");
string testFile = Path.Join(Path.GetTempPath(), string.Format("InsertSqlInputBinding-{0}.ts", DateTime.Now.ToString("yyyy-dd-M--HH-mm-ss")));
File.Copy(originalFile, testFile, true);
AddSqlBindingParams parameters = new AddSqlBindingParams
{
bindingType = BindingType.input,
filePath = testFile,
functionName = "GetArtists_get",
objectName = "[dbo].[table1]",
connectionStringSetting = "SqlConnectionString"
};
AddSqlBindingOperation operation = new AddSqlBindingOperation(parameters);
ResultStatus result = operation.AddBinding();
Assert.True(result.Success);
Assert.IsNull(result.ErrorMessage);
string expectedFileText = File.ReadAllText(Path.Join(testAzureFunctionsFolder, "AzureFunctionsInputBinding.ts"));
string actualFileText = File.ReadAllText(testFile);
Assert.AreEqual(expectedFileText, actualFileText);
}
/// <summary>
/// Verify output binding gets added
/// </summary>
[Test]
public void AddSqlOutputBinding()
{
// copy the original file because the output binding will be inserted into the file
string originalFile = Path.Join(testAzureFunctionsFolder, "AzureFunctionsNoBindings.ts");
string testFile = Path.Join(Path.GetTempPath(), string.Format("InsertSqlOutputBinding-{0}.ts", DateTime.Now.ToString("yyyy-dd-M--HH-mm-ss")));
File.Copy(originalFile, testFile, true);
AddSqlBindingParams parameters = new AddSqlBindingParams
{
bindingType = BindingType.output,
filePath = testFile,
functionName = "NewArtist_post",
objectName = "[dbo].[table1]",
connectionStringSetting = "SqlConnectionString"
};
AddSqlBindingOperation operation = new AddSqlBindingOperation(parameters);
ResultStatus result = operation.AddBinding();
Assert.True(result.Success);
Assert.IsNull(result.ErrorMessage);
string expectedFileText = File.ReadAllText(Path.Join(testAzureFunctionsFolder, "AzureFunctionsOutputBinding.ts"));
string actualFileText = File.ReadAllText(testFile);
Assert.AreEqual(expectedFileText, actualFileText);
}
/// <summary>
/// Verify what happens when specified azure function isn't found
/// </summary>
[Test]
public void NoAzureFunctionForSqlBinding()
{
// copy the original file because the input binding will be inserted into the file
string originalFile = Path.Join(testAzureFunctionsFolder, "AzureFunctionsNoBindings.ts");
string testFile = Path.Join(Path.GetTempPath(), string.Format("NoAzureFunctionForSqlBinding-{0}.ts", DateTime.Now.ToString("yyyy-dd-M--HH-mm-ss")));
File.Copy(originalFile, testFile, true);
AddSqlBindingParams parameters = new AddSqlBindingParams
{
bindingType = BindingType.input,
filePath = testFile,
functionName = "noExistingFunction",
objectName = "[dbo].[table1]",
connectionStringSetting = "SqlConnectionString"
};
AddSqlBindingOperation operation = new AddSqlBindingOperation(parameters);
ResultStatus result = operation.AddBinding();
Assert.False(result.Success);
Assert.NotNull(result.ErrorMessage);
Assert.True(result.ErrorMessage.Equals(SR.CouldntFindAzureFunction("noExistingFunction", testFile)));
}
/// <summary>
/// Verify what happens when there's more than one Azure function with the specified name in the file
/// </summary>
[Test]
public void MoreThanOneAzureFunctionWithSpecifiedName()
{
// copy the original file because the input binding will be inserted into the file
string originalFile = Path.Join(testAzureFunctionsFolder, "AzureFunctionsMultipleSameFunction.ts");
string testFile = Path.Join(Path.GetTempPath(), string.Format("MoreThanOneAzureFunctionWithSpecifiedName-{0}.ts", DateTime.Now.ToString("yyyy-dd-M--HH-mm-ss")));
File.Copy(originalFile, testFile, true);
AddSqlBindingParams parameters = new AddSqlBindingParams
{
bindingType = BindingType.input,
filePath = testFile,
functionName = "GetArtists_get",
objectName = "[dbo].[table1]",
connectionStringSetting = "SqlConnectionString"
};
AddSqlBindingOperation operation = new AddSqlBindingOperation(parameters);
ResultStatus result = operation.AddBinding();
Assert.False(result.Success);
Assert.NotNull(result.ErrorMessage);
Assert.True(result.ErrorMessage.Equals(SR.MoreThanOneAzureFunctionWithName("GetArtists_get", testFile)));
}
}
}