Generator scans assembly's dependencies for functions. Addresses #274.

* This functionality is off by default; to enable, set the project
property FunctionsInDependencies to True.
* Compilers can be too smart. To ensure that a referenced project's DLL
is included in the assembly's dependencies, make sure the assembly's
code explicitly references a member or type from the referenced project.
This commit is contained in:
Katy Shimizu 2019-06-25 13:01:39 -07:00 коммит произвёл Ahmed ElSayed
Родитель 2d39aded66
Коммит 101e4ced4f
22 изменённых файлов: 212 добавлений и 28 удалений

Просмотреть файл

@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net461</TargetFramework>
<FunctionsInDependencies>true</FunctionsInDependencies>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.15" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.29" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FunctionsRefClassicClassLib\FunctionsRefClassicClassLib.csproj" />

Просмотреть файл

@ -0,0 +1,11 @@
using System;
using FunctionsRefSdkClassLib;
namespace FunctionAppNETFramework
{
// Uses references so compiler won't strip them out of the managed module.
class RefUser
{
public Type FunctionsRefSdkClassLibStandard { get => typeof(HttpTriggerRefSdkNETFramework); }
}
}

Просмотреть файл

@ -1,8 +0,0 @@
using System;
namespace FunctionsRefSdkClassLib
{
public class Class1
{
}
}

Просмотреть файл

@ -1,7 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net461</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.29" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,28 @@
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;
namespace FunctionsRefSdkClassLib
{
public class HttpTriggerRefSdkNETFramework
{
[FunctionName("HttpTriggerRefSdkNETFramework")]
public static HttpResponseMessage Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]HttpRequestMessage req, TraceWriter log)
{
//log.Info("C# HTTP trigger function processed a request.");
// parse query parameter
string name = req.GetQueryNameValuePairs()
.FirstOrDefault(q => string.Compare(q.Key, "name", true) == 0)
.Value;
return name == null
? req.CreateResponse(HttpStatusCode.BadRequest, "Please pass a name on the query string or in the request body")
: req.CreateResponse(HttpStatusCode.OK, "Hello " + name);
}
}
}

Просмотреть файл

@ -26,6 +26,22 @@ namespace UnitTestProject1
Assert.AreEqual("\"Hello test\"", responseString);
}
[TestMethod]
public async Task ClassicUnitTestProject_Test_NETFx_Ref()
{
var requestMessage = new HttpRequestMessage(HttpMethod.Get, "https://functions.azurewebsites.net?name=test")
{
Content = new StringContent(""),
Properties = { { HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration() } }
};
var response = await HttpTriggerNETFramework.Run(requestMessage, null);
string responseString = await response.Content.ReadAsStringAsync();
Assert.IsTrue(response.IsSuccessStatusCode);
Assert.AreEqual("\"Hello test\"", responseString);
}
[TestMethod]
public void ClassicUnitTestProject_Empty()
{

Просмотреть файл

@ -26,6 +26,22 @@ namespace UnitTestProject2
Assert.AreEqual("\"Hello test\"", responseString);
}
[TestMethod]
public async Task NewProjectSystem_MsTest_NETFx_Ref()
{
var requestMessage = new HttpRequestMessage(HttpMethod.Get, "https://functions.azurewebsites.net?name=test")
{
Content = new StringContent(""),
Properties = { { HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration() } }
};
var response = await HttpTriggerNETFramework.Run(requestMessage, null);
string responseString = await response.Content.ReadAsStringAsync();
Assert.IsTrue(response.IsSuccessStatusCode);
Assert.AreEqual("\"Hello test\"", responseString);
}
[TestMethod]
public void NewProjectSystem_MsTest_Empty()
{

Просмотреть файл

@ -26,6 +26,21 @@ namespace XUnitTestProject1
Assert.Equal("\"Hello test\"", responseString);
}
[Fact]
public async Task XUnitTest_NETFx_Ref()
{
var requestMessage = new HttpRequestMessage(HttpMethod.Get, "https://functions.azurewebsites.net?name=test")
{
Content = new StringContent(""),
Properties = { { HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration() } }
};
var response = await HttpTriggerNETFramework.Run(requestMessage, null);
string responseString = await response.Content.ReadAsStringAsync();
Assert.True(response.IsSuccessStatusCode);
Assert.Equal("\"Hello test\"", responseString);
}
[Fact]
public void NewProjectSystem_XUnit_Empty()

Просмотреть файл

@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<FunctionsInDependencies>true</FunctionsInDependencies>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.14" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.29" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.CSharp" />

Просмотреть файл

@ -2,7 +2,6 @@ using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;

Просмотреть файл

@ -0,0 +1,11 @@
using System;
using FunctionsRefNETStandard;
namespace FunctionAppNETStandard
{
// Uses references so compiler won't strip them out of the managed module.
class RefUser
{
public Type FunctionsRefNetStandard { get => typeof(HttpTriggerRefNETStandard); }
}
}

Просмотреть файл

@ -1,8 +0,0 @@
using System;
namespace FunctionsRefNETStandard
{
public class Class1
{
}
}

Просмотреть файл

@ -4,4 +4,8 @@
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.29" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,26 @@
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
namespace FunctionsRefNETStandard
{
public class HttpTriggerRefNETStandard
{
[FunctionName("HttpTriggerRefNETStandard")]
public static IActionResult Run([HttpTrigger]HttpRequest req, ILogger log)
{
//log.Info("C# HTTP trigger function processed a request.");
if (req.Query.TryGetValue("name", out StringValues value))
{
return new OkObjectResult($"Hello, {value.First()}");
}
return new BadRequestObjectResult("Please pass a name on the query string");
}
}
}

Просмотреть файл

@ -1,5 +1,6 @@
using System;
using FunctionAppNETStandard;
using FunctionsRefNETStandard;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@ -22,5 +23,17 @@ namespace UnitTestNETFramework
}
}
[TestMethod]
public void NewProjectSystem_MsTest_NETFx_Ref()
{
HttpRequest httpRequest = new DefaultHttpRequest();
var response = HttpTriggerRefNETStandard.Run(httpRequest, null);
if (response is OkObjectResult)
{
Assert.AreEqual("Hello, test", ((OkObjectResult)response).Value);
}
}
}
}

Просмотреть файл

@ -2,6 +2,7 @@ using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using FunctionAppNETStandard;
using FunctionsRefNETStandard;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@ -24,5 +25,17 @@ namespace UnitTestProject2
}
}
[TestMethod]
public void NewProjectSystem_MsTest_NETFx_Ref()
{
HttpRequest httpRequest = new DefaultHttpRequest();
var response = HttpTriggerRefNETStandard.Run(httpRequest, null);
if (response is OkObjectResult)
{
Assert.AreEqual("Hello, test", ((OkObjectResult)response).Value);
}
}
}
}

Просмотреть файл

@ -1,4 +1,5 @@
using FunctionAppNETStandard;
using FunctionsRefNETStandard;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Xunit;
@ -20,5 +21,17 @@ namespace XUnitTestProject1
}
}
[Fact]
public void NewProjectSystem_MsTest_NETFx_Ref()
{
HttpRequest httpRequest = new UnitTestProject2.DefaultHttpRequest();
var response = HttpTriggerRefNETStandard.Run(httpRequest, null);
if (response is OkObjectResult)
{
Assert.Equal("Hello, test", ((OkObjectResult)response).Value);
}
}
}
}

Просмотреть файл

@ -11,6 +11,7 @@ namespace MakeFunctionJson
{
private string _assemblyPath;
private string _outputPath;
private bool _functionsInDependencies;
private readonly HashSet<string> _excludedFunctionNames;
private readonly ILogger _logger;
private readonly IDictionary<string, MethodInfo> _functionNamesSet;
@ -27,7 +28,7 @@ namespace MakeFunctionJson
"kafkatrigger"
};
internal FunctionJsonConverter(ILogger logger, string assemblyPath, string outputPath, IEnumerable<string> excludedFunctionNames = null)
internal FunctionJsonConverter(ILogger logger, string assemblyPath, string outputPath, bool functionsInDependencies, IEnumerable<string> excludedFunctionNames = null)
{
if (logger == null)
{
@ -47,6 +48,7 @@ namespace MakeFunctionJson
_logger = logger;
_assemblyPath = assemblyPath;
_outputPath = outputPath.Trim('"');
_functionsInDependencies = functionsInDependencies;
_excludedFunctionNames = new HashSet<string>(excludedFunctionNames ?? Enumerable.Empty<string>(), StringComparer.OrdinalIgnoreCase);
if (!Path.IsPathRooted(_outputPath))
{
@ -161,7 +163,28 @@ namespace MakeFunctionJson
private bool TryGenerateFunctionJsons()
{
var assembly = Assembly.LoadFrom(_assemblyPath);
var functions = GenerateFunctions(assembly.GetExportedTypes()).ToList();
var assemblyRoot = Path.GetDirectoryName(_assemblyPath);
var exportedTypes = assembly.ExportedTypes;
if (_functionsInDependencies)
{
foreach (var referencedAssembly in assembly.GetReferencedAssemblies())
{
var tryPath = Path.Combine(assemblyRoot, $"{referencedAssembly.Name}.dll");
try
{
var loadedAssembly = Assembly.LoadFrom(tryPath);
exportedTypes = exportedTypes.Concat(loadedAssembly.ExportedTypes);
}
catch (Exception ex)
{
_logger.LogWarning($"Could not evaluate '{referencedAssembly.Name}' for function types. Exception message: {ex.Message}");
}
}
}
var functions = GenerateFunctions(exportedTypes).ToList();
foreach (var function in functions.Where(f => f.HasValue && !f.Value.outputFile.Exists).Select(f => f.Value))
{
function.schema.Serialize(function.outputFile.FullName);

Просмотреть файл

@ -10,14 +10,16 @@ namespace Microsoft.NET.Sdk.Functions.Console
private static void Main(string[] args)
{
var logger = new ConsoleLogger();
if (args.Length < 2 || args.Length > 3)
if (args.Length < 3 || args.Length > 4)
{
logger.LogError("USAGE: <assemblyPath> <outputPath> <excludedFunctionName1;excludedFunctionName2;...>");
logger.LogError("USAGE: <assemblyPath> <outputPath> <functionsInDependencies> <excludedFunctionName1;excludedFunctionName2;...>");
}
else
{
var assemblyPath = args[0].Trim();
var outputPath = args[1].Trim();
var functionsInDependencies = bool.Parse(args[2].Trim());
IEnumerable<string> excludedFunctionNames = Enumerable.Empty<string>();
if (args.Length > 2)
@ -26,7 +28,7 @@ namespace Microsoft.NET.Sdk.Functions.Console
excludedFunctionNames = excludedFunctionNamesArg.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
}
var converter = new FunctionJsonConverter(logger, assemblyPath, outputPath, excludedFunctionNames);
var converter = new FunctionJsonConverter(logger, assemblyPath, outputPath, functionsInDependencies, excludedFunctionNames);
if (!converter.TryRun())
{
logger.LogError("Error generating functions metadata");

Просмотреть файл

@ -43,7 +43,8 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and
OutputPath="$(TargetDir)"
UseNETCoreGenerator="$(UseNETCoreGenerator)"
UseNETFrameworkGenerator="$(UseNETFrameworkGenerator)"
UserProvidedFunctionJsonFiles="@(UserProvidedFunctionJsonFiles)" />
UserProvidedFunctionJsonFiles="@(UserProvidedFunctionJsonFiles)"
FunctionsInDependencies="$(FunctionsInDependencies)"/>
</Target>
<!--

Просмотреть файл

@ -148,7 +148,8 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and
OutputPath="$(FunctionsOutputPath)"
UseNETCoreGenerator="$(UseNETCoreGenerator)"
UseNETFrameworkGenerator="$(UseNETFrameworkGenerator)"
UserProvidedFunctionJsonFiles="@(UserProvidedFunctionJsonFiles)" />
UserProvidedFunctionJsonFiles="@(UserProvidedFunctionJsonFiles)"
FunctionsInDependencies="$(FunctionsInDependencies)" />
</Target>
</Project>

Просмотреть файл

@ -31,6 +31,8 @@ namespace Microsoft.NET.Sdk.Functions.Tasks
public bool GenerateHostJson { get; set; }
public ITaskItem[] UserProvidedFunctionJsonFiles { get; set; }
public bool FunctionsInDependencies { get; set; }
public override bool Execute()
{
@ -89,7 +91,7 @@ namespace Microsoft.NET.Sdk.Functions.Tasks
{
string workingDirectory = isCore ? Path.Combine(baseLocation, NETStandardFolder) : Path.Combine(baseLocation, NETFrameworkFolder);
string exePath = isCore ? DotNetMuxer.MuxerPathOrDefault() : Path.Combine(workingDirectory, "Microsoft.NET.Sdk.Functions.Generator.exe");
string arguments = isCore ? $"Microsoft.NET.Sdk.Functions.Generator.dll \"{TargetPath} \" \"{OutputPath} \"" : $"\"{TargetPath} \" \"{OutputPath} \"";
string arguments = isCore ? $"Microsoft.NET.Sdk.Functions.Generator.dll \"{TargetPath} \" \"{OutputPath} \" \"{FunctionsInDependencies} \"" : $"\"{TargetPath} \" \"{OutputPath} \" \"{FunctionsInDependencies} \"";
string excludedFunctionNamesArg = UserProvidedFunctionJsonFiles?
.Select(f => f.ItemSpec.Replace("/", @"\").Replace(@"\function.json", string.Empty))