Skip to content

refactor(sdk): replace Mono.Cecil with System.Reflection.Metadata - #3469

Open
Jacob Viau (jviau) wants to merge 11 commits into
mainfrom
u/jviau/cecil-to-mlc
Open

refactor(sdk): replace Mono.Cecil with System.Reflection.Metadata#3469
Jacob Viau (jviau) wants to merge 11 commits into
mainfrom
u/jviau/cecil-to-mlc

Conversation

@jviau

Copy link
Copy Markdown
Contributor

Issue describing the changes in this PR

N/A

Summary

Azure.Functions.Sdk scanned extension assemblies with Mono.Cecil to discover WebJobs startups and extension information. This replaces that dependency with the in-box System.Reflection.Metadata APIs, 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 same WebJobsReference / extension-reference results are produced for the same inputs.

Key pieces:

  • FunctionsAssemblyScanner now uses MetadataReader (via ReflectionExtensions / StringTypeProvider helpers) instead of Cecil's ModuleDefinition.
  • Removed FunctionsAssemblyResolver, MonoExtensions, and the Cecil-based ExtensionReference; consolidated shared reflection helpers.
  • ResolveExtensionPackages feeds candidate assemblies to the scanner from the lock file's runtime assemblies directly.

Tests

Added FunctionsAssemblyScannerTests covering single/named/multiple/derived WebJobs startups, the extension-information attribute, and the no-attribute case. Fixtures are real sample projects under test/Resources/AssemblyScanner that 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

  • My changes do not require documentation changes
    • Otherwise: Documentation issue linked to PR
  • My changes should not be added to the release notes for the next release
    • Otherwise: I've added my notes to release_notes.md
  • My changes do not need to be backported to a previous version
    • Otherwise: Backport tracked by issue/PR #issue_or_pr
  • I have added all required tests (Unit tests, E2E tests)

Additional information

Supersedes the previously closed PR, which used a different approach (MetadataLoadContext with a custom assembly resolver). This revision reads metadata without loading assemblies, avoiding the need to resolve or supply core/attribute assemblies at scan time.

…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.
Copilot AI review requested due to automatic review settings July 21, 2026 18:50
@jviau Jacob Viau (jviau) changed the title refactor(sdk): replace Mono.Cecil with System.Reflection.MetadataLoadContext refactor(sdk): replace Mono.Cecil with System.Reflection.Metadata Jul 21, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/Azure.Functions.Sdk/FunctionsAssemblyScanner.cs
@jviau

Jacob Viau (jviau) commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

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 ExtensionMetadataGenerator used today.

EMG will walk the type inheritance chain and also special cases FunctionStartupAttribute(Type type) (here). So this supports 1 additional attribute, but it also supports derived attributes classes which keep the same ctor structure (which is arguably pointless).

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 FunctionStartupAttribute. But I wonder how important it is to add that support. It seems like a partial solution to begin with, it supports inheritance, but not any deviation in the final ctor parameters. Which really questions why support inheritance at all.

…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants