refactor(sdk): replace Mono.Cecil with System.Reflection.Metadata - #3469
refactor(sdk): replace Mono.Cecil with System.Reflection.Metadata#3469Jacob Viau (jviau) wants to merge 11 commits into
Conversation
…Context Port the assembly metadata inspection in Azure.Functions.Sdk from Mono.Cecil to the in-box System.Reflection.MetadataLoadContext (MLC). - Replace Mono.Cecil 0.11.6 with System.Reflection.MetadataLoadContext 10.0.10 and bump Microsoft.Bcl.TimeProvider to 10.0.10. - Rewrite MonoExtensions as ReflectionExtensions over CustomAttributeData/Type. - Make FunctionsAssemblyScanner own a MetadataLoadContext backed by a PathAssemblyResolver built from assembly file paths plus the host runtime directory (required so MLC can resolve core/framework assemblies and terminate the attribute base-type walk). - ResolveExtensionPackages now builds and owns a per-target scanner instead of the old stateless static ExtensionReference.TryGetFromModule call, since MLC must resolve attribute-defining assemblies across sibling packages. - Tolerate unresolvable attribute types (FileNotFoundException / BadImageFormatException) to mirror Cecil's lazy behavior.
The MetadataLoadContext-based scanner could not detect WebJobs extensions. Reading an attribute's type (CustomAttributeData.AttributeType) forces MLC to resolve the assembly that defines the attribute. WebJobsStartupAttribute lives in Microsoft.Azure.WebJobs.Host, which ResolveExtensionCopyLocal intentionally excludes from the extension payload (the host provides it), so the assembly is absent on disk. Resolution threw FileNotFoundException, which was swallowed as a non-match, so every build found 0 extensions and the integration tests failed. Replace the MLC layer with System.Reflection.Metadata (PEReader/MetadataReader) and match attribute types by their metadata name without resolving external assemblies, mirroring the original Cecil behavior: - Add MetadataAttributeReader (attribute type-name lookup, resolution-free base walk) and StringTypeProvider (decodes ctor args, representing every type as its string name so System.Type arguments do not trigger resolution). - Rewrite ExtensionReference and WebJobsReference.FromModule to read from a MetadataReader; the scanner opens a PEReader per file and materializes results before disposal. - Make FunctionsAssemblyScanner static and drop the shared resolver/path set; ResolveExtensionPackages no longer builds a per-target resolution set. - Swap the System.Reflection.MetadataLoadContext package for System.Reflection.Metadata.
Add unit/semi-integration coverage for FunctionsAssemblyScanner. Tests compile throwaway extension assemblies at runtime with Roslyn, then delete the attribute-defining "host" assembly before scanning, faithfully reproducing the production scenario where Microsoft.Azure.WebJobs.Host is intentionally absent from the extension payload. Covers WebJobs startup detection (explicit/derived names, multiple startups, base-type walk), ExtensionInformation extension references, empty results, and null/empty argument guards. Verified on net10.0 and net472.
Replace runtime Roslyn compilation of throwaway assemblies with static sample extension projects that are P2P-referenced with ReferenceOutputAssembly="false" and copied into an "extensions" folder next to the test assembly. This scans real build artifacts and removes the Microsoft.CodeAnalysis.CSharp test dependency. The attribute-defining TestExtension.Abstractions assembly is deliberately excluded from the deployed extensions, preserving the "attribute assembly absent" scenario that motivated the metadata-based scanner.
…Scanner Relocate the FunctionsAssemblyScanner sample extension fixtures from test/FunctionsAssemblyScanner.Samples to test/Resources/AssemblyScanner to group them with the other test resources. Update the sample project glob in the test project and the doc comments accordingly. No behavior change.
Replace the hand-rolled TestExtension.Abstractions project with references to the real packages that define the marker attributes: Microsoft.Azure.WebJobs (WebJobsStartupAttribute) and Microsoft.Azure.Functions.Worker.Extensions.Abstractions (ExtensionInformationAttribute). Only each extension's own primary assembly is copied next to the tests, so the package assemblies that define the attributes remain absent from the scanned payload. This preserves the "attribute-defining assembly absent" scenario while exercising the scanner against the genuine attribute types instead of stand-ins.
…nfo projects TestExtension.Startup previously carried both a WebJobsStartup attribute and an ExtensionInformation attribute, forcing it to reference both the WebJobs and Worker.Extensions.Abstractions packages. Split the extension-information scenario into a dedicated TestExtension.Information project so each fixture references only the one package that defines the attribute it exercises. Also fix the fixture deployment so it works on clean builds: the previous target injected copy items via BeforeTargets=GetCopyToOutputDirectoryItems, which is silently ignored when injecting into the test project itself, so fixtures only appeared in the output from stale prior builds. Replace it with an explicit Copy after CopyFilesToOutputDirectory.
There was a problem hiding this comment.
Pull request overview
Refactors Azure.Functions.Sdk extension-assembly scanning to use System.Reflection.Metadata / PEReader rather than Mono.Cecil, enabling attribute detection by metadata name without resolving attribute-defining assemblies (which are absent from extension payloads), and updates tasks/tests accordingly.
Changes:
- Replaced Cecil-based startup/extension attribute scanning with metadata-based scanning (including base-type walking within the scanned assembly).
- Updated extension package resolution and metadata-writing tasks to use the new scanner entry points.
- Added new unit tests and resource fixture projects to validate scanning behavior when attribute-defining assemblies are not present.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/Resources/AssemblyScanner/TestExtension.Startup/TestExtension.Startup.csproj | New fixture project producing a single WebJobs startup attribute case. |
| test/Resources/AssemblyScanner/TestExtension.Startup/Startup.cs | New fixture source with a single [assembly: WebJobsStartup]. |
| test/Resources/AssemblyScanner/TestExtension.Plain/TestExtension.Plain.csproj | New “no marker attributes” fixture project. |
| test/Resources/AssemblyScanner/TestExtension.Plain/NotAnExtension.cs | New fixture source for the “no attributes” scan case. |
| test/Resources/AssemblyScanner/TestExtension.NamedStartup/TestExtension.NamedStartup.csproj | New fixture project for explicit WebJobs startup name argument. |
| test/Resources/AssemblyScanner/TestExtension.NamedStartup/Startup.cs | New fixture source with named WebJobs startup attribute. |
| test/Resources/AssemblyScanner/TestExtension.MultipleStartups/TestExtension.MultipleStartups.csproj | New fixture project for multiple startup attributes in one assembly. |
| test/Resources/AssemblyScanner/TestExtension.MultipleStartups/Startups.cs | New fixture source with multiple [assembly: WebJobsStartup] usages. |
| test/Resources/AssemblyScanner/TestExtension.Information/TestExtension.Information.csproj | New fixture project for ExtensionInformationAttribute scanning. |
| test/Resources/AssemblyScanner/TestExtension.Information/ExtensionInfo.cs | New fixture source with [assembly: ExtensionInformation(...)]. |
| test/Resources/AssemblyScanner/TestExtension.DerivedStartup/TestExtension.DerivedStartup.csproj | New fixture project for derived-attribute base-type walking. |
| test/Resources/AssemblyScanner/TestExtension.DerivedStartup/Startup.cs | New fixture source defining a derived attribute and applying it at assembly level. |
| test/Resources/AssemblyScanner/Directory.Build.props | Shared props to standardize fixture project build settings and import repo-root props. |
| test/Azure.Functions.Sdk.Tests/FunctionsAssemblyScannerTests.cs | Added tests validating WebJobs and extension-information scanning behavior under metadata-only constraints. |
| test/Azure.Functions.Sdk.Tests/Azure.Functions.Sdk.Tests.csproj | Builds/copies fixture assemblies to an extensions folder for scanner tests. |
| src/Azure.Functions.Sdk/WebJobsReference.FromModule.cs | Reworked WebJobs reference discovery to operate on MetadataReader instead of Cecil. |
| src/Azure.Functions.Sdk/Tasks/Extensions/WriteExtensionMetadata.cs | Updated task to call new scanner API directly (no resolver/scanner instance). |
| src/Azure.Functions.Sdk/Tasks/Extensions/ResolveExtensionPackages.cs | Updated extension reference detection to use the new scanner implementation. |
| src/Azure.Functions.Sdk/StringTypeProvider.cs | New helper to decode custom attribute ctor args (including Type) from metadata without resolution. |
| src/Azure.Functions.Sdk/ReflectionExtensions.cs | New metadata-based helpers for attribute type-name extraction and base-type walking. |
| src/Azure.Functions.Sdk/MonoExtensions.cs | Removed Cecil-specific helper extensions. |
| src/Azure.Functions.Sdk/FunctionsAssemblyScanner.cs | Replaced Cecil/resolver-driven scanner with metadata/PEReader-based scanner APIs. |
| src/Azure.Functions.Sdk/FunctionsAssemblyResolver.cs | Removed Cecil assembly resolver. |
| src/Azure.Functions.Sdk/ExtensionReference.cs | Removed Cecil-based extension reference implementation (folded into scanner). |
| src/Azure.Functions.Sdk/Azure.Functions.Sdk.csproj | Dropped Mono.Cecil dependency and added System.Reflection.Metadata package reference. |
Comments suppressed due to low confidence (1)
src/Azure.Functions.Sdk/WebJobsReference.FromModule.cs:102
- GetName uses string.IndexOf without specifying StringComparison, which is culture-sensitive by default. Since this logic is intended to be deterministic and match the WebJobs SDK behavior, it should use ordinal comparisons.
name = GetSimpleTypeName(startupTypeName);
int idx = name.IndexOf("WebJobsStartup");
if (idx < 0)
{
idx = name.IndexOf("Startup");
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Realized a gap in this approach (and maybe in the previous one as well): when resolving WebJobsStartupAttribute, it is trying to support inherited versions of that attribute. But the support is very limited. Will need to rethink how to better support that, and if it is possible at all. Okay it is a gap not far off from what we have today in EMG will walk the type inheritance chain and also special cases What this implementation does not support (and the previous one) is walking an inheritance chain that spans multiple assemblies. This can be added I think, same with supporting |
…emType The _IncludeSampleExtensions target used an MSBuild GetTargetPath call to locate each sample extension's output DLL, but GetTargetPath returns a predicted path without building the project. On the Linux CI agent the referenced sample had not necessarily been built when the copy ran, producing MSB3030 "could not copy ... because it was not found". Capture each sample's actual primary output via OutputItemType on the ProjectReference (populated during ResolveProjectReferences) and copy those items directly. This removes path prediction and the assumption that the reference was already built. Only the extension's own assembly is captured, so the attribute-defining packages stay out of the scanned payload as the fixtures require.
Replace the copy-to-extensions-folder deployment with a generated source file. The _GenerateSampleExtensionPaths target captures each sample's build output (@(SampleExtensionOutput) from ResolveProjectReferences) and writes SampleExtensions.g.cs into the intermediate output, mapping each sample assembly name to its absolute output path. The scanner tests read those paths in place. Each sample still sits alone in its own output directory (its attribute-defining packages are not beside it), preserving the invariant that the scanner must match marker attributes as external TypeReferences without resolving the defining assembly. Scanning absolute in-place paths also removes the net472 shadow-copy workaround (Assembly.CodeBase), since the paths no longer depend on where the test host runs from.
Drop the generated path-map source file. The sample ProjectReferences (ReferenceOutputAssembly=false) still build each extension in place; the tests now locate each sample's output by convention relative to the test assembly: anchor at its real output directory, read the configuration from that path, and walk up to the shared test root to reach Resources/AssemblyScanner/<Name>/bin/<Config>/netstandard2.0/<Name>.dll. Each sample is still read from its own output directory, where its attribute-defining packages are absent, preserving the invariant that the scanner must match marker attributes as external TypeReferences without resolving the defining assembly. net472 still anchors via Assembly.CodeBase so the convention resolves against the real build output rather than the shadow-copy cache.
Issue describing the changes in this PR
N/A
Summary
Azure.Functions.Sdkscanned extension assemblies with Mono.Cecil to discover WebJobs startups and extension information. This replaces that dependency with the in-boxSystem.Reflection.MetadataAPIs, dropping a third-party dependency in favor of the platform.The scanner reads assembly metadata directly rather than loading assemblies, so it never needs to resolve the attribute-defining assemblies (
Microsoft.Azure.WebJobs.Host,Worker.Extensions.Abstractions), which are intentionally absent from the scanned extension payload. Marker attributes are matched by metadata type name, and the derived-attribute case is handled by walking base-type references. Behavior is unchanged: the sameWebJobsReference/ extension-reference results are produced for the same inputs.Key pieces:
FunctionsAssemblyScannernow usesMetadataReader(viaReflectionExtensions/StringTypeProviderhelpers) instead of Cecil'sModuleDefinition.FunctionsAssemblyResolver,MonoExtensions, and the Cecil-basedExtensionReference; consolidated shared reflection helpers.ResolveExtensionPackagesfeeds candidate assemblies to the scanner from the lock file's runtime assemblies directly.Tests
Added
FunctionsAssemblyScannerTestscovering single/named/multiple/derived WebJobs startups, the extension-information attribute, and the no-attribute case. Fixtures are real sample projects undertest/Resources/AssemblyScannerthat reference the genuine attribute packages but deploy only their own primary assembly, reproducing the production "attribute-defining assembly absent" condition. Full suite passes on net10.0 and net472.Pull request checklist
release_notes.mdAdditional information
Supersedes the previously closed PR, which used a different approach (
MetadataLoadContextwith a custom assembly resolver). This revision reads metadata without loading assemblies, avoiding the need to resolve or supply core/attribute assemblies at scan time.