From 3cbe01e2e0264c167edabefd010b580554729b68 Mon Sep 17 00:00:00 2001 From: Vincent Bitter Date: Fri, 27 Aug 2021 22:27:14 +0200 Subject: [PATCH 1/7] Add fake dependency file to prove issue --- ...ons.Worker.Extensions.OpenApi.Tests.csproj | 3 +++ .../TestData/FakeDependencyFile.deps.json | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 test/Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Tests/TestData/FakeDependencyFile.deps.json diff --git a/test/Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Tests/Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Tests.csproj b/test/Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Tests/Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Tests.csproj index 8f2712c7..66585192 100644 --- a/test/Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Tests/Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Tests.csproj +++ b/test/Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Tests/Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Tests.csproj @@ -36,6 +36,9 @@ Always + + Always + diff --git a/test/Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Tests/TestData/FakeDependencyFile.deps.json b/test/Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Tests/TestData/FakeDependencyFile.deps.json new file mode 100644 index 00000000..78486474 --- /dev/null +++ b/test/Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Tests/TestData/FakeDependencyFile.deps.json @@ -0,0 +1,22 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v5.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v5.0": { + "Microsoft.Azure.Functions.Worker.Extensions.OpenApi/1.0.0": { + "dependencies": { + "Microsoft.Azure.Core.NewtonsoftJson": "1.0.0", + "Microsoft.Azure.Functions.Worker.Core": "1.1.0", + "Microsoft.Azure.Functions.Worker.Extensions.Http": "3.0.12", + "Microsoft.Azure.WebJobs.Extensions.OpenApi.Core": "1.0.0" + }, + "runtime": { + "Microsoft.Azure.Functions.Worker.Extensions.OpenApi.dll": {} + } + } + } + } +} From 77361382bd6cb1a14b73aa175cba84b7b3a7be6e Mon Sep 17 00:00:00 2001 From: Vincent Bitter Date: Fri, 27 Aug 2021 22:29:07 +0200 Subject: [PATCH 2/7] Add unit test to prove exact issue --- .../OpenApiHttpTriggerContextTests.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Tests/OpenApiHttpTriggerContextTests.cs b/test/Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Tests/OpenApiHttpTriggerContextTests.cs index f809b56f..3df8352f 100644 --- a/test/Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Tests/OpenApiHttpTriggerContextTests.cs +++ b/test/Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Tests/OpenApiHttpTriggerContextTests.cs @@ -31,6 +31,18 @@ public async Task Given_Type_When_Initiated_Then_It_Should_Return_ApplicationAss assembly.DefinedTypes.Select(p => p.FullName).Should().Contain(ti.FullName); } + [TestMethod] + public async Task Given_Type_With_Referenced_Project_When_Initiated_Then_It_Should_Return_ApplicationAssemblyOfRootAssembly() + { + var location = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName; + var context = new OpenApiHttpTriggerContext(); + + var assembly = (await context.SetApplicationAssemblyAsync(location, false)) + .ApplicationAssembly; + + assembly.FullName.Should().Be(typeof(OpenApiHttpTriggerContextTests).Assembly.FullName); + } + [DataTestMethod] [DataRow(typeof(IOpenApiHttpTriggerContext))] [DataRow(typeof(OpenApiHttpTriggerContext))] From b9ca664d3bcad51eb4a81851e9c4152a96205185 Mon Sep 17 00:00:00 2001 From: Vincent Bitter Date: Fri, 27 Aug 2021 22:29:49 +0200 Subject: [PATCH 3/7] Scan all .deps.json files and determine root dll --- .../OpenApiHttpTriggerContext.cs | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.Azure.Functions.Worker.Extensions.OpenApi/OpenApiHttpTriggerContext.cs b/src/Microsoft.Azure.Functions.Worker.Extensions.OpenApi/OpenApiHttpTriggerContext.cs index 81f1ff97..b02ae02d 100644 --- a/src/Microsoft.Azure.Functions.Worker.Extensions.OpenApi/OpenApiHttpTriggerContext.cs +++ b/src/Microsoft.Azure.Functions.Worker.Extensions.OpenApi/OpenApiHttpTriggerContext.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -235,27 +236,43 @@ private string GetRuntimePath(string functionAppDirectory, bool appendBin) // This method relies on the dependency manifest file to find the function app runtime dll file. // It can be either .deps.json or function.deps.json. In most cases, at least the // function.deps.json should exist, but in case no manifest exists, it will throw the exception. + // In case there are multiple .deps.json files, the root project will be picked, based on the + // dependencies mentioned in the .deps.json files. private async Task GetRuntimeFilenameAsync(string functionAppDirectory) { var files = Directory.GetFiles(functionAppDirectory, "*.deps.json", SearchOption.AllDirectories); - var file = files.FirstOrDefault(); - if (file.IsNullOrWhiteSpace()) + if (!files.Any()) { throw new InvalidOperationException("Invalid function app directory"); } + var dependencyManifests = new List(); + foreach (var file in files) { + dependencyManifests.Add(await GetDependencyManifestAsync(file)); + } + + var runtimes = dependencyManifests + .Select(manifest => manifest.Targets[manifest.RuntimeTarget.Name].First()) + .Select(target => new + { + Name = target.Key.Split("/").First(), + FileName = target.Value.Runtime.First().Key, + Dependencies = target.Value.Dependencies.Keys + }); + + var referencedRuntimes = runtimes.SelectMany(d => d.Dependencies); + return runtimes.FirstOrDefault(r => !referencedRuntimes.Contains(r.Name))?.FileName; + } + + private static async Task GetDependencyManifestAsync(string file) + { var serialised = default(string); using (var reader = File.OpenText(file)) { serialised = await reader.ReadToEndAsync(); } - var manifesto = JsonConvert.DeserializeObject(serialised); - var runtimeTarget = manifesto.RuntimeTarget.Name; - var runtimes = manifesto.Targets[runtimeTarget].Values; - var runtime = runtimes.First().Runtime.First().Key; - - return runtime; + return JsonConvert.DeserializeObject(serialised); } private Assembly GetAssembly(object instance) From 12b286230407fb6a6c186ffc34c0d060e9c3f866 Mon Sep 17 00:00:00 2001 From: Vincent Bitter Date: Mon, 30 Aug 2021 12:03:47 +0200 Subject: [PATCH 4/7] Fix style issue --- .../OpenApiHttpTriggerContext.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Azure.Functions.Worker.Extensions.OpenApi/OpenApiHttpTriggerContext.cs b/src/Microsoft.Azure.Functions.Worker.Extensions.OpenApi/OpenApiHttpTriggerContext.cs index b02ae02d..cb5ac743 100644 --- a/src/Microsoft.Azure.Functions.Worker.Extensions.OpenApi/OpenApiHttpTriggerContext.cs +++ b/src/Microsoft.Azure.Functions.Worker.Extensions.OpenApi/OpenApiHttpTriggerContext.cs @@ -247,7 +247,8 @@ private async Task GetRuntimeFilenameAsync(string functionAppDirectory) } var dependencyManifests = new List(); - foreach (var file in files) { + foreach (var file in files) + { dependencyManifests.Add(await GetDependencyManifestAsync(file)); } From 30bd8d266a3a46b6564cbd32c6c5d03495d3deed Mon Sep 17 00:00:00 2001 From: Vincent Bitter Date: Mon, 30 Aug 2021 14:43:00 +0200 Subject: [PATCH 5/7] Add test for in-proc extension --- ...re.WebJobs.Extensions.OpenApi.Tests.csproj | 3 +++ .../OpenApiHttpTriggerContextTests.cs | 12 ++++++++++ .../TestData/FakeDependencyFile.deps.json | 22 +++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 test/Microsoft.Azure.WebJobs.Extensions.OpenApi.Tests/TestData/FakeDependencyFile.deps.json diff --git a/test/Microsoft.Azure.WebJobs.Extensions.OpenApi.Tests/Microsoft.Azure.WebJobs.Extensions.OpenApi.Tests.csproj b/test/Microsoft.Azure.WebJobs.Extensions.OpenApi.Tests/Microsoft.Azure.WebJobs.Extensions.OpenApi.Tests.csproj index 97b062dc..85c5ddec 100644 --- a/test/Microsoft.Azure.WebJobs.Extensions.OpenApi.Tests/Microsoft.Azure.WebJobs.Extensions.OpenApi.Tests.csproj +++ b/test/Microsoft.Azure.WebJobs.Extensions.OpenApi.Tests/Microsoft.Azure.WebJobs.Extensions.OpenApi.Tests.csproj @@ -37,6 +37,9 @@ Always + + Always + diff --git a/test/Microsoft.Azure.WebJobs.Extensions.OpenApi.Tests/OpenApiHttpTriggerContextTests.cs b/test/Microsoft.Azure.WebJobs.Extensions.OpenApi.Tests/OpenApiHttpTriggerContextTests.cs index 39c242c3..c4950ee7 100644 --- a/test/Microsoft.Azure.WebJobs.Extensions.OpenApi.Tests/OpenApiHttpTriggerContextTests.cs +++ b/test/Microsoft.Azure.WebJobs.Extensions.OpenApi.Tests/OpenApiHttpTriggerContextTests.cs @@ -30,6 +30,18 @@ public async Task Given_Type_When_Initiated_Then_It_Should_Return_ApplicationAss assembly.DefinedTypes.Select(p => p.FullName).Should().Contain(ti.FullName); } + [TestMethod] + public async Task Given_Type_With_Referenced_Project_When_Initiated_Then_It_Should_Return_ApplicationAssemblyOfRootAssembly() + { + var location = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName; + var context = new OpenApiHttpTriggerContext(); + + var assembly = (await context.SetApplicationAssemblyAsync(location, false)) + .ApplicationAssembly; + + assembly.FullName.Should().Be(typeof(OpenApiHttpTriggerContextTests).Assembly.FullName); + } + [DataTestMethod] [DataRow(typeof(IOpenApiHttpTriggerContext))] [DataRow(typeof(OpenApiHttpTriggerContext))] diff --git a/test/Microsoft.Azure.WebJobs.Extensions.OpenApi.Tests/TestData/FakeDependencyFile.deps.json b/test/Microsoft.Azure.WebJobs.Extensions.OpenApi.Tests/TestData/FakeDependencyFile.deps.json new file mode 100644 index 00000000..706124f6 --- /dev/null +++ b/test/Microsoft.Azure.WebJobs.Extensions.OpenApi.Tests/TestData/FakeDependencyFile.deps.json @@ -0,0 +1,22 @@ +{ + "runtimeTarget": { + "name": ".NETStandard,Version=v2.0/", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETStandard,Version=v2.0": {}, + ".NETStandard,Version=v2.0/": { + "Microsoft.Azure.WebJobs.Extensions.OpenApi/1.0.0": { + "dependencies": { + "Microsoft.Azure.WebJobs.Extensions.OpenApi.Core": "1.0.0", + "Microsoft.Azure.WebJobs.Script.Abstractions": "1.0.0-preview", + "NETStandard.Library": "2.0.3" + }, + "runtime": { + "Microsoft.Azure.WebJobs.Extensions.OpenApi.dll": {} + } + } + } + } +} From 702cb2fad2ca9bdeadd06f35a90cbc1cc1733193 Mon Sep 17 00:00:00 2001 From: Vincent Bitter Date: Mon, 30 Aug 2021 14:47:25 +0200 Subject: [PATCH 6/7] Fix issue with multiple deps.json files in in-proc extension --- .../OpenApiHttpTriggerContext.cs | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.Azure.WebJobs.Extensions.OpenApi/OpenApiHttpTriggerContext.cs b/src/Microsoft.Azure.WebJobs.Extensions.OpenApi/OpenApiHttpTriggerContext.cs index 53007b79..937a3b9d 100644 --- a/src/Microsoft.Azure.WebJobs.Extensions.OpenApi/OpenApiHttpTriggerContext.cs +++ b/src/Microsoft.Azure.WebJobs.Extensions.OpenApi/OpenApiHttpTriggerContext.cs @@ -1,11 +1,11 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; -using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core; using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Abstractions; using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Configurations; @@ -236,27 +236,44 @@ private string GetRuntimePath(string functionAppDirectory, bool appendBin) // This method relies on the dependency manifest file to find the function app runtime dll file. // It can be either .deps.json or function.deps.json. In most cases, at least the // function.deps.json should exist, but in case no manifest exists, it will throw the exception. + // In case there are multiple .deps.json files, the root project will be picked, based on the + // dependencies mentioned in the .deps.json files. private async Task GetRuntimeFilenameAsync(string functionAppDirectory) { var files = Directory.GetFiles(functionAppDirectory, "*.deps.json", SearchOption.AllDirectories); - var file = files.FirstOrDefault(); - if (file.IsNullOrWhiteSpace()) + if (!files.Any()) { throw new InvalidOperationException("Invalid function app directory"); } + var dependencyManifests = new List(); + foreach (var file in files) + { + dependencyManifests.Add(await GetDependencyManifestAsync(file)); + } + + var runtimes = dependencyManifests + .Select(manifest => manifest.Targets[manifest.RuntimeTarget.Name].First()) + .Select(target => new + { + Name = target.Key.Split('/').First(), + FileName = target.Value.Runtime.First().Key, + Dependencies = target.Value.Dependencies.Keys + }); + + var referencedRuntimes = runtimes.SelectMany(d => d.Dependencies); + return runtimes.FirstOrDefault(r => !referencedRuntimes.Contains(r.Name))?.FileName; + } + + private static async Task GetDependencyManifestAsync(string file) + { var serialised = default(string); using (var reader = File.OpenText(file)) { serialised = await reader.ReadToEndAsync(); } - var manifesto = JsonConvert.DeserializeObject(serialised); - var runtimeTarget = manifesto.RuntimeTarget.Name; - var runtimes = manifesto.Targets[runtimeTarget].Values; - var runtime = runtimes.First().Runtime.First().Key; - - return runtime; + return JsonConvert.DeserializeObject(serialised); } private Assembly GetAssembly(object instance) From 89e29303a7fed198abcae13c97d9a212f02b8a60 Mon Sep 17 00:00:00 2001 From: Vincent Bitter Date: Mon, 30 Aug 2021 14:48:02 +0200 Subject: [PATCH 7/7] Split by char should perform better than string and keeps it in sync with the in-proc version --- .../OpenApiHttpTriggerContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Azure.Functions.Worker.Extensions.OpenApi/OpenApiHttpTriggerContext.cs b/src/Microsoft.Azure.Functions.Worker.Extensions.OpenApi/OpenApiHttpTriggerContext.cs index cb5ac743..f1b38fb9 100644 --- a/src/Microsoft.Azure.Functions.Worker.Extensions.OpenApi/OpenApiHttpTriggerContext.cs +++ b/src/Microsoft.Azure.Functions.Worker.Extensions.OpenApi/OpenApiHttpTriggerContext.cs @@ -256,7 +256,7 @@ private async Task GetRuntimeFilenameAsync(string functionAppDirectory) .Select(manifest => manifest.Targets[manifest.RuntimeTarget.Name].First()) .Select(target => new { - Name = target.Key.Split("/").First(), + Name = target.Key.Split('/').First(), FileName = target.Value.Runtime.First().Key, Dependencies = target.Value.Dependencies.Keys });