Add route to GetAzureFunctions request + cleanup (#1521)

* Add route to GetAzureFunctions request + cleanup

* update comments

* comments

* Add comment
This commit is contained in:
Charles Gagnon
2022-05-31 10:22:20 -07:00
committed by GitHub
parent 5fdad0edc8
commit 076ed9644b
7 changed files with 265 additions and 53 deletions

View File

@@ -3,6 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System;
using System.Collections.Generic;
@@ -12,8 +13,10 @@ namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions
{
internal static class AzureFunctionsUtils
{
public const string functionAttributeText = "FunctionName";
public const string net5FunctionAttributeText = "Function";
private const string FUNCTION_NAME_ATTRIBUTE_NAME = "FunctionName";
private const string NET5_FUNCTION_ATTRIBUTE_NAME = "Function";
private const string HTTP_TRIGGER_ATTRIBUTE_NAME = "HttpTrigger";
private const string ROUTE_ARGUMENT_NAME = "Route";
/// <summary>
/// Gets all the methods in the syntax tree with an Azure Function attribute
@@ -31,19 +34,74 @@ namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions
}
// get all the method declarations with the FunctionName attribute
IEnumerable<MethodDeclarationSyntax> methodsWithFunctionAttributes = methodDeclarations.Where(md => md.AttributeLists.Where(a => a.Attributes.Where(attr => attr.Name.ToString().Equals(functionAttributeText)).Any()).Any());
IEnumerable<MethodDeclarationSyntax> methodsWithFunctionAttributes = methodDeclarations.Where(md => md.AttributeLists.Where(a => a.Attributes.Where(attr => attr.Name.ToString().Equals(FUNCTION_NAME_ATTRIBUTE_NAME)).Any()).Any());
return methodsWithFunctionAttributes;
}
/// <summary>
/// Gets the route from an HttpTrigger attribute if specified
/// </summary>
/// <param name="m">The method</param>
/// <returns>The name of the route, or null if no route is specified (or there isn't an HttpTrigger binding)</returns>
public static string? GetHttpRoute(this MethodDeclarationSyntax m)
{
return m
.ParameterList
.Parameters // Get all the parameters for the method
.SelectMany(p =>
p.AttributeLists // Get a list of all attributes on any of the parameters
.SelectMany(al => al.Attributes)
).Where(a => a.Name.ToString().Equals(HTTP_TRIGGER_ATTRIBUTE_NAME) // Find any HttpTrigger attributes
).FirstOrDefault() // Get the first one available - there should only ever be 0 or 1
?.ArgumentList // Get all the arguments for the attribute
?.Arguments
.Where(a => a.ChildNodes().OfType<NameEqualsSyntax>().Where(nes => nes.Name.ToString().Equals(ROUTE_ARGUMENT_NAME)).Any()) // Find the Route argument - it should always be a named argument
.FirstOrDefault()
?.ChildNodes()
.OfType<ExpressionSyntax>() // Find the child identifier node with our value
.Where(le => le.Kind() != SyntaxKind.NullLiteralExpression) // Skip the null expressions so they aren't ToString()'d into "null"
.FirstOrDefault()
?.ToString()
.TrimStart('$') // Remove $ from interpolated string, since this will always be outside the quotes we can just trim
.Trim('\"'); // Trim off the quotes from the string value - additional quotes at the beginning and end aren't valid syntax so it's fine to trim
}
/// <summary>
/// Gets the function name from the FunctionName attribute on a method
/// </summary>
/// <param name="m">The method</param>
/// <returns>The function name, or an empty string if the name attribute doesn't exist</returns>
public static string GetFunctionName(this MethodDeclarationSyntax m)
{
// Note that we return an empty string as the default because a null name isn't valid - every function
// should have a name. So we should never actually hit that scenario, but just to be safe we return the
// empty string as the default in case we hit some unexpected edge case.
return m
.AttributeLists // Get all the attribute lists on the method
.Select(a =>
a.Attributes.Where(
attr => attr.Name.ToString().Equals(AzureFunctionsUtils.FUNCTION_NAME_ATTRIBUTE_NAME) // Find any FunctionName attributes
).FirstOrDefault() // Get the first one available - there should only ever be 0 or 1
).Where(a => // Filter out any that didn't have a FunctionName attribute
a != null
).FirstOrDefault() // Get the first one available - there should only ever be 0 or 1
?.ArgumentList // Get all the arguments for the attribute
?.Arguments
.FirstOrDefault() // The first argument is the function name
?.ToString()
.TrimStart('$') // Remove $ from interpolated string, since this will always be outside the quotes we can just trim
.Trim('\"') ?? ""; // Trim off the quotes from the string value - additional quotes at the beginning and end aren't valid syntax so it's fine to trim
}
/// <summary>
/// Checks if any of the method declarations have .NET 5 style Azure Function attributes
/// .NET 5 AFs use the Function attribute, while .NET 3.1 AFs use FunctionName attritube
/// .NET 5 AFs use the Function attribute, while .NET 3.1 AFs use FunctionName attribute
/// </summary>
public static bool HasNet5StyleAzureFunctions(IEnumerable<MethodDeclarationSyntax> methodDeclarations)
{
// get all the method declarations with the Function attribute
return methodDeclarations.Any(md => md.AttributeLists.Any(al => al.Attributes.Any(attr => attr.Name.ToString().Equals(net5FunctionAttributeText))));
return methodDeclarations.Any(md => md.AttributeLists.Any(al => al.Attributes.Any(attr => attr.Name.ToString().Equals(NET5_FUNCTION_ATTRIBUTE_NAME))));
}
}
}

View File

@@ -3,7 +3,6 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions.Contracts
{
@@ -15,15 +14,39 @@ namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions.Contracts
/// <summary>
/// Gets or sets the filePath
/// </summary>
public string filePath { get; set; }
public string FilePath { get; set; }
}
public class AzureFunction
{
/// <summary>
/// The name of the function
/// </summary>
public string Name { get; set; }
/// <summary>
/// The route of the HttpTrigger binding if one exists on this function
/// </summary>
public string? Route { get; set; }
public AzureFunction(string name, string? route)
{
this.Name = name;
this.Route = route;
}
}
/// <summary>
/// Parameters returned from a get Azure functions request
/// </summary>
public class GetAzureFunctionsResult : ResultStatus
public class GetAzureFunctionsResult
{
public string[] azureFunctions { get; set; }
public AzureFunction[] AzureFunctions { get; set; }
public GetAzureFunctionsResult(AzureFunction[] azureFunctions)
{
this.AzureFunctions = azureFunctions;
}
}
/// <summary>

View File

@@ -37,7 +37,7 @@ namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions
{
try
{
string text = File.ReadAllText(Parameters.filePath);
string text = File.ReadAllText(Parameters.FilePath);
SyntaxTree tree = CSharpSyntaxTree.ParseText(text);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();
@@ -45,20 +45,9 @@ namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions
// get all the method declarations with the FunctionName attribute
IEnumerable<MethodDeclarationSyntax> methodsWithFunctionAttributes = AzureFunctionsUtils.GetMethodsWithFunctionAttributes(root);
// Get FunctionName attributes
IEnumerable<AttributeSyntax> functionNameAttributes = methodsWithFunctionAttributes.Select(md => md.AttributeLists.Select(a => a.Attributes.Where(attr => attr.Name.ToString().Equals(AzureFunctionsUtils.functionAttributeText)).First()).First());
var azureFunctions = methodsWithFunctionAttributes.Select(m => new AzureFunction(m.GetFunctionName(), m.GetHttpRoute())).ToArray();
// Get the function names in the FunctionName attributes
IEnumerable<AttributeArgumentSyntax> nameArgs = functionNameAttributes.Select(a => a.ArgumentList.Arguments.First());
// Remove quotes from around the names
string[] aFNames = nameArgs.Select(ab => ab.ToString().Trim('\"')).ToArray();
return new GetAzureFunctionsResult()
{
Success = true,
azureFunctions = aFNames
};
return new GetAzureFunctionsResult(azureFunctions);
}
catch (Exception ex)
{

View File

@@ -0,0 +1,35 @@
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;
namespace Company.Namespace
{
public class ArtistsApi
{
/// <summary>
/// Function with basic name
/// </summary>
[FunctionName("WithName")]
public IActionResult WithName([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "artists")] HttpRequest req)
{
throw new NotImplementedException();
}
private const string interpolated = "interpolated";
/// <summary>
/// Function with interpolated string as name
/// </summary>
[FunctionName($"{interpolated}String")]
public async IActionResult InterpolatedString([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "artists")] Artist body, HttpRequest req)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,89 @@
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 AzureFunctionsRoute
{
/// <summary>
/// Tests binding with a route specified
/// </summary>
[FunctionName("WithRoute")]
public IActionResult WithRoute([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "withRoute")] HttpRequest req, [Sql("select * from [dbo].[table1]", CommandType = System.Data.CommandType.Text, ConnectionStringSetting = "SqlConnectionString")] IEnumerable<Object> result)
{
throw new NotImplementedException();
}
private const string interpolated = "interpolated";
/// <summary>
/// Tests binding with a route specified using an interpolated string
/// </summary>
[FunctionName("InterpolatedString")]
public IActionResult InterpolatedString([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = $"{interpolated}String")] HttpRequest req, [Sql("select * from [dbo].[table1]", CommandType = System.Data.CommandType.Text, ConnectionStringSetting = "SqlConnectionString")] IEnumerable<Object> result)
{
throw new NotImplementedException();
}
/// <summary>
/// Tests binding with a route specified that has $'s on the beginning and end
/// </summary>
[FunctionName("WithDollarSigns")]
public IActionResult WithDollarSigns([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "$withDollarSigns$")] HttpRequest req, [Sql("select * from [dbo].[table1]", CommandType = System.Data.CommandType.Text, ConnectionStringSetting = "SqlConnectionString")] IEnumerable<Object> result)
{
throw new NotImplementedException();
}
/// <summary>
/// Tests binding with a route specified and no spaces between tokens
/// </summary>
[FunctionName("WithRouteNoSpaces")]
public IActionResult WithRouteNoSpaces([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route="withRouteNoSpaces")] HttpRequest req, [Sql("select * from [dbo].[table1]", CommandType = System.Data.CommandType.Text, ConnectionStringSetting = "SqlConnectionString")] IEnumerable<Object> result)
{
throw new NotImplementedException();
}
/// <summary>
/// Tests binding with a route specified and no spaces between tokens
/// </summary>
[FunctionName("WithRouteExtraSpaces")]
public IActionResult WithRouteExtraSpaces([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "withRouteExtraSpaces")] HttpRequest req, [Sql("select * from [dbo].[table1]", CommandType = System.Data.CommandType.Text, ConnectionStringSetting = "SqlConnectionString")] IEnumerable<Object> result)
{
throw new NotImplementedException();
}
/// <summary>
/// Tests binding with a null route specified
/// </summary>
[FunctionName("WithNullRoute")]
public IActionResult WithNullRoute([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, [Sql("select * from [dbo].[table1]", CommandType = System.Data.CommandType.Text, ConnectionStringSetting = "SqlConnectionString")] IEnumerable<Object> result)
{
throw new NotImplementedException();
}
/// <summary>
/// Tests binding with a null route specified
/// </summary>
[FunctionName("NoRoute")]
public IActionResult NoRoute([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req, [Sql("select * from [dbo].[table1]", CommandType = System.Data.CommandType.Text, ConnectionStringSetting = "SqlConnectionString")] IEnumerable<Object> result)
{
throw new NotImplementedException();
}
/// <summary>
/// Tests binding with an empty route specified
/// </summary>
[FunctionName("EmptyRoute")]
public IActionResult EmptyRoute([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "")] HttpRequest req, [Sql("select * from [dbo].[table1]", CommandType = System.Data.CommandType.Text, ConnectionStringSetting = "SqlConnectionString")] IEnumerable<Object> result)
{
throw new NotImplementedException();
}
}
}

View File

@@ -102,12 +102,12 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.AzureFunctions
AddSqlBindingOperation operation = new AddSqlBindingOperation(parameters);
ResultStatus result = operation.AddBinding();
Assert.True(result.Success);
Assert.IsNull(result.ErrorMessage);
Assert.That(result.Success, Is.True);
Assert.That(result.ErrorMessage, Is.Null);
string expectedFileText = File.ReadAllText(Path.Join(testAzureFunctionsFolder, "AzureFunctionsOutputBindingAsync.cs"));
string actualFileText = File.ReadAllText(testFile);
Assert.AreEqual(expectedFileText, actualFileText);
Assert.That(actualFileText, Is.EqualTo(expectedFileText));
}
/// <summary>
@@ -133,9 +133,9 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.AzureFunctions
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)));
Assert.That(result.Success, Is.False);
Assert.That(result.ErrorMessage, Is.Not.Null);
Assert.That(result.ErrorMessage, Is.EqualTo(SR.CouldntFindAzureFunction("noExistingFunction", testFile)));
}
/// <summary>
@@ -161,33 +161,30 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.AzureFunctions
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)));
Assert.That(result.Success, Is.False);
Assert.That(result.ErrorMessage, Is.Not.Null);
Assert.That(result.ErrorMessage, Is.EqualTo(SR.MoreThanOneAzureFunctionWithName("GetArtists_get", testFile)));
}
/// <summary>
/// Verify getting the names of Azure functions in a file
/// </summary>
[Test]
public void GetAzureFunctions()
public void GetAzureFunctionNames()
{
string testFile = Path.Join(testAzureFunctionsFolder, "AzureFunctionsNoBindings.cs");
string testFile = Path.Join(testAzureFunctionsFolder, "AzureFunctionsName.cs");
GetAzureFunctionsParams parameters = new GetAzureFunctionsParams
{
filePath = testFile
FilePath = testFile
};
GetAzureFunctionsOperation operation = new GetAzureFunctionsOperation(parameters);
GetAzureFunctionsResult result = operation.GetAzureFunctions();
Assert.True(result.Success);
Assert.Null(result.ErrorMessage);
Assert.AreEqual(3, result.azureFunctions.Length);
Assert.AreEqual(result.azureFunctions[0], "GetArtists_get");
Assert.AreEqual(result.azureFunctions[1], "NewArtist_post");
Assert.AreEqual(result.azureFunctions[2], "NewArtists_post");
Assert.That(result.AzureFunctions.Length, Is.EqualTo(2));
Assert.That(result.AzureFunctions[0].Name, Is.EqualTo("WithName"));
Assert.That(result.AzureFunctions[1].Name, Is.EqualTo("{interpolated}String"));
}
/// <summary>
@@ -202,15 +199,13 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.AzureFunctions
fstream.Close();
GetAzureFunctionsParams parameters = new GetAzureFunctionsParams
{
filePath = testFile
FilePath = testFile
};
GetAzureFunctionsOperation operation = new GetAzureFunctionsOperation(parameters);
GetAzureFunctionsResult result = operation.GetAzureFunctions();
Assert.True(result.Success);
Assert.Null(result.ErrorMessage);
Assert.AreEqual(0, result.azureFunctions.Length);
Assert.That(result.AzureFunctions.Length, Is.EqualTo(0));
}
/// <summary>
@@ -223,13 +218,40 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.AzureFunctions
GetAzureFunctionsParams parameters = new GetAzureFunctionsParams
{
filePath = testFile
FilePath = testFile
};
GetAzureFunctionsOperation operation = new GetAzureFunctionsOperation(parameters);
Exception ex = Assert.Throws<Exception>(() => { operation.GetAzureFunctions(); });
Assert.AreEqual(SR.SqlBindingsNet5NotSupported, ex.Message);
Assert.That(ex.Message, Is.EqualTo(SR.SqlBindingsNet5NotSupported));
}
/// <summary>
/// Verify getting the routes of Azure Functions in a file
/// </summary>
[Test]
public void GetAzureFunctionsRoute()
{
string testFile = Path.Join(testAzureFunctionsFolder, "AzureFunctionsRoute.cs");
GetAzureFunctionsParams parameters = new GetAzureFunctionsParams
{
FilePath = testFile
};
GetAzureFunctionsOperation operation = new GetAzureFunctionsOperation(parameters);
GetAzureFunctionsResult result = operation.GetAzureFunctions();
Assert.That(result.AzureFunctions.Length, Is.EqualTo(8));
Assert.That(result.AzureFunctions[0].Route, Is.EqualTo("withRoute"));
Assert.That(result.AzureFunctions[1].Route, Is.EqualTo("{interpolated}String"));
Assert.That(result.AzureFunctions[2].Route, Is.EqualTo("$withDollarSigns$"));
Assert.That(result.AzureFunctions[3].Route, Is.EqualTo("withRouteNoSpaces"));
Assert.That(result.AzureFunctions[4].Route, Is.EqualTo("withRouteExtraSpaces"));
Assert.That(result.AzureFunctions[5].Route, Is.Null, "Route specified as null should be null");
Assert.That(result.AzureFunctions[6].Route, Is.Null, "No route specified should be null");
Assert.That(result.AzureFunctions[7].Route, Is.EqualTo(""));
}
}
}

View File

@@ -39,12 +39,8 @@
</Content>
</ItemGroup>
<ItemGroup>
<Compile Remove="AzureFunctions\AzureFunctionTestFiles\AzureFunctionsInputBinding.cs" />
<Compile Remove="AzureFunctions\AzureFunctionTestFiles\AzureFunctionsMultipleSameFunction.cs" />
<Compile Remove="AzureFunctions\AzureFunctionTestFiles\AzureFunctionsNoBindings.cs" />
<Compile Remove="AzureFunctions\AzureFunctionTestFiles\AzureFunctionsOutputBinding.cs" />
<Compile Remove="AzureFunctions\AzureFunctionTestFiles\AzureFunctionsNet5.cs" />
<Compile Remove="AzureFunctions\AzureFunctionTestFiles\AzureFunctionsOutputBindingAsync.cs" />
<Compile Remove="AzureFunctions\AzureFunctionTestFiles\*" />
<None Include="AzureFunctions\AzureFunctionTestFiles\*" />
</ItemGroup>
<ItemGroup>
<Folder Include="DacFx\Dacpacs\" />