Skip to content

Commit 8ee1fb5

Browse files
authored
[pack] moving ExtensionsMetadataGenerator to use Mono.Cecil (#6052)
1 parent 8e4912e commit 8ee1fb5

File tree

9 files changed

+141
-102
lines changed

9 files changed

+141
-102
lines changed

tools/ExtensionsMetadataGenerator/src/ExtensionsMetadataGenerator.Console/ExtensionsMetadataGenerator.Console.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
</PropertyGroup>
1313

1414
<ItemGroup>
15+
<PackageReference Include="Mono.Cecil" Version="0.11.1" />
1516
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
1617
</ItemGroup>
1718
</Project>

tools/ExtensionsMetadataGenerator/src/ExtensionsMetadataGenerator.Console/ExtensionsMetadataGenerator.cs

Lines changed: 80 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.IO;
77
using System.Linq;
88
using System.Reflection;
9+
using Mono.Cecil;
910
#if !NET46
1011
#endif
1112

@@ -14,6 +15,7 @@ namespace ExtensionsMetadataGenerator
1415
public class ExtensionsMetadataGenerator
1516
{
1617
private const string WebJobsStartupAttributeType = "Microsoft.Azure.WebJobs.Hosting.WebJobsStartupAttribute";
18+
private const string FunctionsStartupAttributeType = "Microsoft.Azure.Functions.Extensions.DependencyInjection.FunctionsStartupAttribute";
1719

1820
// These assemblies are always loaded by the functions runtime and should not be listed in extensions.json
1921
private static readonly string[] ExcludedAssemblies = new[] { "Microsoft.Azure.WebJobs.Extensions.dll", "Microsoft.Azure.WebJobs.Extensions.Http.dll" };
@@ -43,23 +45,14 @@ public static void Generate(string sourcePath, string outputPath, ConsoleLogger
4345
{
4446
try
4547
{
46-
Assembly assembly = Assembly.LoadFrom(path);
47-
var currExtensionReferences = GenerateExtensionReferences(assembly);
48+
var currExtensionReferences = GenerateExtensionReferences(path, logger);
4849
extensionReferences.AddRange(currExtensionReferences);
4950

5051
foreach (var foundRef in currExtensionReferences)
5152
{
5253
logger.LogMessage($"Found extension: {foundRef.TypeName}");
5354
}
5455
}
55-
catch (Exception ex) when (ex is FileNotFoundException || ex is BadImageFormatException)
56-
{
57-
// Don't log this as an error. This will almost always happen due to some publishing artifacts (i.e. Razor) existing in the
58-
// functions bin folder without all of their dependencies present, or native package artifacts being copied to the bin folder
59-
// These will almost never have Functions extensions, so we don't want to write out errors every time there is a build.
60-
// This message can be seen with detailed logging enabled.
61-
logger.LogMessage($"Could not evaluate '{Path.GetFileName(path)}' for extension metadata. If this assembly contains a Functions extension, ensure that all dependent assemblies exist in '{sourcePath}'. If this assembly does not contain any Functions extensions, this message can be ignored. Exception message: {ex.Message}");
62-
}
6356
catch (Exception ex)
6457
{
6558
logger.LogError($"Could not evaluate '{Path.GetFileName(path)}' for extension metadata. Exception message: {ex.Message}");
@@ -82,44 +75,109 @@ public static string GenerateExtensionsJson(IEnumerable<ExtensionReference> exte
8275
return json;
8376
}
8477

85-
public static bool IsWebJobsStartupAttributeType(Type attributeType)
78+
public static bool IsWebJobsStartupAttributeType(TypeReference attributeType, ConsoleLogger logger)
8679
{
87-
Type currentType = attributeType;
80+
TypeReference currentAttributeType = attributeType;
8881

89-
while (currentType != null)
82+
while (currentAttributeType != null)
9083
{
91-
if (string.Equals(currentType.FullName, WebJobsStartupAttributeType, StringComparison.OrdinalIgnoreCase))
84+
if (string.Equals(currentAttributeType.FullName, WebJobsStartupAttributeType, StringComparison.OrdinalIgnoreCase))
9285
{
9386
return true;
9487
}
9588

96-
currentType = currentType.BaseType;
89+
try
90+
{
91+
currentAttributeType = currentAttributeType.Resolve()?.BaseType;
92+
}
93+
catch (FileNotFoundException ex)
94+
{
95+
// Don't log this as an error. This will almost always happen due to some publishing artifacts (i.e. Razor) existing
96+
// in the functions bin folder without all of their dependencies present. These will almost never have Functions extensions,
97+
// so we don't want to write out errors every time there is a build. This message can be seen with detailed logging enabled.
98+
string attributeTypeName = GetReflectionFullName(attributeType);
99+
string fileName = Path.GetFileName(attributeType.Module.FileName);
100+
logger.LogMessage($"Could not determine whether the attribute type '{attributeTypeName}' used in the assembly '{fileName}' derives from '{WebJobsStartupAttributeType}' because the assembly defining its base type could not be found. Exception message: {ex.Message}");
101+
return false;
102+
}
97103
}
98104

99105
return false;
100106
}
101107

102-
public static IEnumerable<ExtensionReference> GenerateExtensionReferences(Assembly assembly)
108+
public static IEnumerable<ExtensionReference> GenerateExtensionReferences(string fileName, ConsoleLogger logger)
103109
{
104-
var startupAttributes = assembly.GetCustomAttributes()
105-
.Where(a => IsWebJobsStartupAttributeType(a.GetType()));
110+
BaseAssemblyResolver resolver = new DefaultAssemblyResolver();
111+
resolver.AddSearchDirectory(Path.GetDirectoryName(fileName));
112+
113+
ReaderParameters readerParams = new ReaderParameters { AssemblyResolver = resolver };
114+
115+
AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(fileName, readerParams);
116+
117+
var startupAttributes = assembly.Modules.SelectMany(p => p.GetCustomAttributes())
118+
.Where(a => IsWebJobsStartupAttributeType(a.AttributeType, logger));
106119

107120
List<ExtensionReference> extensionReferences = new List<ExtensionReference>();
108121
foreach (var attribute in startupAttributes)
109122
{
110-
var nameProperty = attribute.GetType().GetProperty("Name");
111-
var typeProperty = attribute.GetType().GetProperty("WebJobsStartupType");
123+
var typeProperty = attribute.ConstructorArguments.ElementAtOrDefault(0);
124+
var nameProperty = attribute.ConstructorArguments.ElementAtOrDefault(1);
125+
126+
TypeDefinition typeDef = (TypeDefinition)typeProperty.Value;
127+
string assemblyQualifiedName = Assembly.CreateQualifiedName(typeDef.Module.Assembly.FullName, GetReflectionFullName(typeDef));
128+
129+
string name;
130+
131+
// Because we're now using static analysis we can't rely on the constructor running so have to get the name ourselves.
132+
if (string.Equals(attribute.AttributeType.FullName, FunctionsStartupAttributeType, StringComparison.OrdinalIgnoreCase))
133+
{
134+
// FunctionsStartup always uses the type name as the name.
135+
name = typeDef.Name;
136+
}
137+
else
138+
{
139+
// WebJobsStartup does some trimming.
140+
name = GetName((string)nameProperty.Value, typeDef);
141+
}
112142

113143
var extensionReference = new ExtensionReference
114144
{
115-
Name = (string)nameProperty.GetValue(attribute),
116-
TypeName = ((Type)typeProperty.GetValue(attribute)).AssemblyQualifiedName
145+
Name = name,
146+
TypeName = assemblyQualifiedName
117147
};
118148

119149
extensionReferences.Add(extensionReference);
120150
}
121151

122152
return extensionReferences;
123153
}
154+
155+
// Copying the WebJobsStartup constructor logic from:
156+
// https://github.com/Azure/azure-webjobs-sdk/blob/e5417775bcb8c8d3d53698932ca8e4e265eac66d/src/Microsoft.Azure.WebJobs.Host/Hosting/WebJobsStartupAttribute.cs#L33-L47.
157+
private static string GetName(string name, TypeDefinition startupTypeDef)
158+
{
159+
if (string.IsNullOrEmpty(name))
160+
{
161+
// for a startup class named 'CustomConfigWebJobsStartup' or 'CustomConfigStartup',
162+
// default to a name 'CustomConfig'
163+
name = startupTypeDef.Name;
164+
int idx = name.IndexOf("WebJobsStartup");
165+
if (idx < 0)
166+
{
167+
idx = name.IndexOf("Startup");
168+
}
169+
if (idx > 0)
170+
{
171+
name = name.Substring(0, idx);
172+
}
173+
}
174+
175+
return name;
176+
}
177+
178+
public static string GetReflectionFullName(TypeReference typeRef)
179+
{
180+
return typeRef.FullName.Replace("/", "+");
181+
}
124182
}
125183
}

tools/ExtensionsMetadataGenerator/src/ExtensionsMetadataGenerator.Console/Program.cs

Lines changed: 0 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,11 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5-
using System.IO;
6-
using System.Reflection;
7-
using System.Runtime.Loader;
8-
using System.Threading;
95

106
namespace ExtensionsMetadataGenerator.Console
117
{
128
public class Program
139
{
14-
private static readonly Assembly _thisAssembly = typeof(Program).Assembly;
15-
1610
public static void Main(string[] args)
1711
{
1812
if (args.Length < 2)
@@ -28,7 +22,6 @@ public static void Main(string[] args)
2822

2923
try
3024
{
31-
AssemblyLoader.Initialize(sourcePath, logger);
3225
ExtensionsMetadataGenerator.Generate(sourcePath, args[1], logger);
3326
}
3427
catch (Exception ex)
@@ -37,64 +30,5 @@ public static void Main(string[] args)
3730
throw;
3831
}
3932
}
40-
41-
private class AssemblyLoader
42-
{
43-
private static int _initialized;
44-
45-
public static void Initialize(string basePath, ConsoleLogger logger)
46-
{
47-
if (Interlocked.CompareExchange(ref _initialized, 1, 0) == 0)
48-
{
49-
AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
50-
{
51-
logger.LogMessage($"Resolving assembly: '{args.Name}'");
52-
53-
try
54-
{
55-
string assemblyName = new AssemblyName(args.Name).Name;
56-
string assemblyPath = Path.Combine(basePath, assemblyName + ".dll");
57-
58-
// This indicates a recursive lookup. Abort here to prevent stack overflow.
59-
if (args.RequestingAssembly == _thisAssembly)
60-
{
61-
logger.LogMessage($"Cannot load '{assemblyName}'. Aborting assembly resolution.");
62-
return null;
63-
}
64-
65-
if (File.Exists(assemblyPath))
66-
{
67-
Assembly assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
68-
logger.LogMessage($"Assembly '{assemblyName}' loaded from '{assemblyPath}'.");
69-
return assembly;
70-
}
71-
72-
try
73-
{
74-
// If the assembly file is not found, it may be a runtime assembly for a different
75-
// runtime version (i.e. the Function app assembly targets .NET Core 2.2, yet this
76-
// process is running 2.0). In that case, just try to return the currently-loaded assembly,
77-
// even if it's the wrong version; we won't be running it, just reflecting.
78-
Assembly assembly = Assembly.Load(assemblyName);
79-
logger.LogMessage($"Assembly '{assemblyName}' loaded.");
80-
return assembly;
81-
}
82-
catch (Exception ex)
83-
{
84-
// We'll already log an error if this happens; this gives a little more details if debug is enabled.
85-
logger.LogMessage($"Unable to find fallback for assembly '{assemblyName}'. {ex}");
86-
}
87-
88-
return null;
89-
}
90-
catch (Exception ex)
91-
{
92-
logger.LogError($"Error resolving assembly '{args.Name}': {ex}");
93-
throw;
94-
}
95-
};
96-
}
97-
}
98-
}
9933
}
10034
}

tools/ExtensionsMetadataGenerator/src/ExtensionsMetadataGenerator/ExtensionsMetadataGenerator.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk" InitialTargets="UpdateRuntimeAssemblies">
22
<Import Project="..\..\build\metadatagenerator.props" />
33
<PropertyGroup>
4-
<VersionPrefix>1.1.8</VersionPrefix>
4+
<Version>1.2.0</Version>
55
<OutputType>Library</OutputType>
66
<TargetFrameworks>netstandard2.0;net46</TargetFrameworks>
77
<AssemblyName>Microsoft.Azure.WebJobs.Script.ExtensionsMetadataGenerator</AssemblyName>

tools/ExtensionsMetadataGenerator/test/ExtensionsMetadataGeneratorTests/ExtensionsMetadataGeneratorTests.cs

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,20 @@
44
using System;
55
using System.IO;
66
using System.Linq;
7+
using ExtensionsMetadataGenerator;
78
using ExtensionsMetadataGenerator.Console;
9+
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
810
using Microsoft.Azure.WebJobs.Hosting;
11+
using Mono.Cecil;
912
using Newtonsoft.Json.Linq;
1013
using Xunit;
1114

1215
namespace ExtensionsMetadataGeneratorTests
1316
{
1417
public class ExtensionsMetadataGeneratorTests
1518
{
19+
private ConsoleLogger _logger = new ConsoleLogger();
20+
1621
[Fact]
1722
public void Generator_DifferentTargetFrameworks()
1823
{
@@ -42,17 +47,17 @@ public void Generator_DifferentTargetFrameworks()
4247
// plus the two from the 2.1 and 2.2 test projects.
4348
JToken extensions = json["extensions"];
4449
int startups = extensions.Count();
45-
Assert.Equal(4, startups);
50+
Assert.Equal(6, startups);
4651

4752
Assert.Single(extensions, e => e["name"].ToString() == "Foo" && e["typeName"].ToString().StartsWith("ExtensionsMetadataGeneratorTests.FooWebJobsStartup, ExtensionsMetadataGeneratorTests"));
4853
Assert.Single(extensions, e => e["name"].ToString() == "BarExtension" && e["typeName"].ToString().StartsWith("ExtensionsMetadataGeneratorTests.BarWebJobsStartup, ExtensionsMetadataGeneratorTests"));
54+
Assert.Single(extensions, e => e["name"].ToString() == "TestFunctionsStartup" && e["typeName"].ToString().StartsWith("ExtensionsMetadataGeneratorTests.TestFunctionsStartup, ExtensionsMetadataGeneratorTests"));
4955
Assert.Single(extensions, e => e["name"].ToString() == "Startup" && e["typeName"].ToString().StartsWith("TestProject_Core21.Startup"));
5056
Assert.Single(extensions, e => e["name"].ToString() == "Startup" && e["typeName"].ToString().StartsWith("TestProject_Core22.Startup"));
57+
Assert.Single(extensions, e => e["name"].ToString() == "Startup" && e["typeName"].ToString().StartsWith("TestProject_Razor.Startup"));
5158

52-
// We cannot read TestProject_Razor.dll because it has a dependency we cannot find. Make sure
53-
// we log correctly even though we've skipped this assembly.
54-
Assert.Contains("Cannot load 'Microsoft.AspNetCore.Razor.Runtime'. Aborting assembly resolution.", log.ToString());
55-
Assert.Contains("Could not evaluate 'TestProject_Razor.dll' for extension metadata. If this assembly contains a Functions extension, ensure that all dependent assemblies exist in ", log.ToString());
59+
// We log a message here, but have successfully found the TestProject_Razor.Startup.
60+
Assert.Contains("Could not determine whether the attribute type 'Microsoft.AspNetCore.Razor.Hosting.RazorExtensionAssemblyNameAttribute' used in the assembly 'TestProject_Razor.dll' derives from 'Microsoft.Azure.WebJobs.Hosting.WebJobsStartupAttribute'", log.ToString());
5661
}
5762
finally
5863
{
@@ -63,42 +68,53 @@ public void Generator_DifferentTargetFrameworks()
6368
[Fact]
6469
public void GenerateExtensionReferences_Succeeds()
6570
{
66-
var assembly = GetType().Assembly;
67-
var references = ExtensionsMetadataGenerator.ExtensionsMetadataGenerator.GenerateExtensionReferences(assembly).ToArray();
68-
Assert.Equal(2, references.Length);
71+
var fileName = GetType().Assembly.Location;
72+
var references = ExtensionsMetadataGenerator.ExtensionsMetadataGenerator.GenerateExtensionReferences(fileName, _logger).ToArray();
73+
Assert.Equal(3, references.Length);
6974

7075
Assert.Equal("Foo", references[0].Name);
7176
Assert.Equal(typeof(FooWebJobsStartup).AssemblyQualifiedName, references[0].TypeName);
7277

7378
Assert.Equal("BarExtension", references[1].Name);
7479
Assert.Equal(typeof(BarWebJobsStartup).AssemblyQualifiedName, references[1].TypeName);
80+
81+
Assert.Equal(nameof(TestFunctionsStartup), references[2].Name);
82+
Assert.Equal(typeof(TestFunctionsStartup).AssemblyQualifiedName, references[2].TypeName);
7583
}
7684

7785
[Fact]
7886
public void GenerateExtensionsJson_Succeeds()
7987
{
80-
var assembly = GetType().Assembly;
81-
var references = ExtensionsMetadataGenerator.ExtensionsMetadataGenerator.GenerateExtensionReferences(assembly).ToArray();
88+
var assembly = GetType().Assembly.Location;
89+
var references = ExtensionsMetadataGenerator.ExtensionsMetadataGenerator.GenerateExtensionReferences(assembly, _logger).ToArray();
8290
string json = ExtensionsMetadataGenerator.ExtensionsMetadataGenerator.GenerateExtensionsJson(references);
8391

8492
var root = JObject.Parse(json);
8593
var extensions = root["extensions"];
94+
Assert.Equal(3, extensions.Count());
8695

8796
Assert.Equal("Foo", extensions[0]["name"]);
8897
Assert.Equal(typeof(FooWebJobsStartup).AssemblyQualifiedName, extensions[0]["typeName"]);
8998

9099
Assert.Equal("BarExtension", extensions[1]["name"]);
91100
Assert.Equal(typeof(BarWebJobsStartup).AssemblyQualifiedName, extensions[1]["typeName"]);
101+
102+
Assert.Equal(nameof(TestFunctionsStartup), extensions[2]["name"]);
103+
Assert.Equal(typeof(TestFunctionsStartup).AssemblyQualifiedName, extensions[2]["typeName"]);
92104
}
93105

94106
[Theory]
95107
[InlineData(typeof(WebJobsStartupAttribute), true)]
96108
[InlineData(typeof(TestStartupAttribute), true)]
109+
[InlineData(typeof(FunctionsStartupAttribute), true)]
97110
[InlineData(typeof(CLSCompliantAttribute), false)]
98111
[InlineData(typeof(Attribute), false)]
99112
public void IsWebJobsStartupAttributeType_CorrectlyIdentifiesAttributes(Type attributeType, bool isWebJobsType)
100113
{
101-
bool result = ExtensionsMetadataGenerator.ExtensionsMetadataGenerator.IsWebJobsStartupAttributeType(attributeType);
114+
ModuleDefinition module = ModuleDefinition.ReadModule(attributeType.Assembly.Location);
115+
TypeReference typeRef = module.GetTypes().Single(p => p.FullName == attributeType.FullName);
116+
117+
bool result = ExtensionsMetadataGenerator.ExtensionsMetadataGenerator.IsWebJobsStartupAttributeType(typeRef, _logger);
102118

103119
Assert.Equal(isWebJobsType, result);
104120
}

tools/ExtensionsMetadataGenerator/test/ExtensionsMetadataGeneratorTests/ExtensionsMetadataGeneratorTests.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
</ItemGroup>
1111

1212
<ItemGroup>
13-
<PackageReference Include="Microsoft.Azure.WebJobs" Version="3.0.4" />
13+
<PackageReference Include="Microsoft.Azure.WebJobs" Version="3.0.5" />
14+
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.0.0" />
1415
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
1516
<PackageReference Include="xunit" Version="2.4.0" />
1617
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />

0 commit comments

Comments
 (0)