Skip to content

Add V2 attribute-based programming model (worker indexing)#1130

Draft
andystaples wants to merge 1 commit into
devfrom
andystaples/worker-indexing-v2
Draft

Add V2 attribute-based programming model (worker indexing)#1130
andystaples wants to merge 1 commit into
devfrom
andystaples/worker-indexing-v2

Conversation

@andystaples

@andystaples andystaples commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the V2 attribute-based programming model for Azure Functions PowerShell Worker. Functions are defined using PowerShell attributes (e.g. [AzFunction()], [HttpTrigger()]) directly in .psm1 files — no function.json files needed. The worker indexes functions by parsing the AST at startup, without executing user code.

What's included

Core indexing engine (src/WorkerIndexing/):

  • FunctionIndexer — Recursively scans .ps1/.psm1 files, parses the AST for [AzFunction()]-decorated functions, and produces RpcFunctionMetadata for the host
  • AttributeToBindingConverter — Converts attribute AST nodes to BindingInfo and raw binding JSON via reflection-based property discovery; handles implicit output bindings (e.g. HTTP Response), cardinality, data type mapping
  • .funcignore support — Parses glob patterns from .funcignore at the app root to exclude files/directories during scanning

30+ attribute classes (src/Attributes/):

  • Triggers: HttpTrigger, TimerTrigger, QueueTrigger, BlobTrigger, EventHubTrigger, CosmosDBTrigger, ServiceBusTrigger, EventGridTrigger, SignalRTrigger, OrchestrationTrigger, ActivityTrigger, EntityTrigger, GenericTrigger
  • Inputs: BlobInput, CosmosDBInput, TableInput, SignalRInput, DurableClient, GenericInputBinding
  • Outputs: HttpOutput, QueueOutput, BlobOutput, CosmosDBOutput, TableOutput, EventHubOutput, EventGridOutput, ServiceBusOutput, SignalROutput, GenericOutputBinding
  • Base classes with RequiredProperties for validation

Validation (in FunctionIndexer.BuildFunctionMetadata):

  • Exactly one trigger binding per function
  • Unique binding names within a function
  • Required properties present (e.g. Schedule on TimerTrigger, DatabaseName+ContainerName on CosmosDBTrigger)
  • Graceful per-file error handling — invalid files are skipped with a warning, valid files continue indexing

Runtime integration:

  • RequestProcessor wires up FunctionIndexer for FunctionsMetadataRequest responses
  • FunctionLoader/DependencyManager path resolution adapted for V2 (app root vs. per-function subdirectory)
  • Pipeline output leak warning in PowerShellManager.ExecuteUserCode()
  • worker.config.json declares WorkerIndexing capability

E2E test infrastructure:

  • test/E2E/TestFunctionAppV2/ — Full V2 test app with HTTP, Timer, Queue, Blob, EventHub, CosmosDB, and Durable functions
  • E2E test helpers updated to use DefaultAzureCredential (identity-based auth) instead of connection strings

Example app (examples/V2App/):

  • HTTP, Timer, and Queue trigger examples demonstrating the V2 model
  • Includes .funcignore, README.md with V1-vs-V2 comparison

Tests: 428 unit tests passing (90 new), 20/23 E2E tests passing


Remaining Gaps / Known Issues

  • Editor/tooling warnings: No VS Code language server or editor integration for V2 attributes yet (e.g. squiggles for missing required properties, IntelliSense for attribute parameters). This would be a separate extension contribution.
  • Documentation: The public-facing docs (function authoring guide, migration guide from V1 to V2) are not included in this PR. A create_new_worker_instructions.md update is also pending.
  • No function.json coexistence story: Currently V1 (function.json) and V2 (attributes) are mutually exclusive per app. There's no explicit guard preventing a mixed app — could lead to confusing behavior if someone has both.

Design Decisions & Alternatives

Attributes vs. Register-AzureFunction cmdlet

The chosen approach uses PowerShell class attributes parsed via the AST at load time (no code execution). An alternative would be a Register-AzureFunction cmdlet that users call imperatively:

# Alternative: cmdlet-based registration
Register-AzureFunction -Name 'HttpExample' -ScriptBlock {
    param($Request)
    Push-OutputBinding -Name Response -Value 'Hello'
} -Trigger @{ Type = 'httpTrigger'; AuthLevel = 'anonymous' }

Why attributes were chosen:

  • Enables static analysis / indexing without executing user code (security, performance)
  • Consistent with the Python V2 model (decorators) and .NET isolated model (attributes)
  • More declarative — the function definition is self-contained
  • Better tooling potential (editor completions, linting)

Tradeoffs:

  • Cmdlet approach would be more "PowerShell-idiomatic" (pipeline-friendly, dynamic)
  • Cmdlet approach wouldn't require AST parsing, just running the script
  • Attributes require users to learn a new syntax pattern, though it mirrors existing Azure Functions SDKs
  • Cmdlet approach could support dynamic function registration (e.g. generating functions from config)

AST parsing vs. script execution for indexing

The indexer uses System.Management.Automation.Language.Parser.ParseFile() to extract function definitions without executing them. This is a deliberate security choice — user code may have side effects, module imports, or credential access that shouldn't happen at indexing time. The tradeoff is that dynamically constructed attributes (e.g. splatting, variables in attribute arguments) are not supported.

Implicit output bindings

HTTP triggers automatically get an implicit Response output binding (matching the host's behavior). If the user explicitly declares [HttpOutput()] $Response, the explicit binding wins. This matches the Python V2 model's behavior.

.funcignore implementation

A simple custom glob-to-regex converter is used rather than adding a Microsoft.Extensions.FileSystemGlobbing package dependency. The patterns in .funcignore files in the wild are simple (literal names, * wildcards, ** recursive), so a full globbing library was unnecessary.

GenericTrigger/GenericInputBinding/GenericOutputBinding

These "escape hatch" attributes allow users to define bindings for extension types not covered by the built-in attribute classes (e.g. Kafka, RabbitMQ). The user specifies Type, Connection, and a Properties hashtable that gets flattened into the raw binding JSON.

Needs Further Investigation / Testing

  • Cardinality handling: The Cardinality property on triggers (e.g. EventHubTrigger) maps to Many/One on BindingInfo. Need to verify the host correctly handles all cardinality values, especially for batch-mode triggers.
  • Data type mapping: DataType on bindings maps to String/Binary/Stream/Int — need to verify the host accepts all of these and that they correctly influence parameter marshaling.
  • Warm restart behavior: When the Functions host does a warm restart (placeholder specialization), does the worker re-index? The FunctionsMetadataRequest path should handle this, but it needs E2E verification.
  • Durable SDK interaction: The external Durable SDK (AzureFunctions.PowerShell.Durable.SDK) works with V2 in E2E tests, but the full matrix of Durable patterns (sub-orchestrations, eternal orchestrations, continue-as-new) hasn't been tested with V2.
  • Module-scoped variables in .psm1: Since multiple functions can live in one .psm1, need to verify that module-scoped $script: variables work correctly across function invocations and don't leak state.
  • Large app performance: The AST parser is called per-file. For apps with many .psm1 files or large files, indexing time should be benchmarked. The .funcignore and ExcludedDirectories filtering helps, but there's no caching across cold starts.
  • Custom binding extensions: The GenericTrigger escape hatch works for arbitrary binding types, but we should verify it works with all extension bundle versions and custom extensions loaded outside the bundle.
  • TableInput filter expressions: The Filter and Take properties on TableInput need verification with the Tables extension — these may require specific OData filter syntax.
  • SignalR negotiate function: The SignalRInput binding (connection info / negotiate) — need to test the full SignalR handshake flow with V2.

Implement the V2 programming model for Azure Functions PowerShell Worker,
enabling function definitions via PowerShell attributes instead of function.json.

Core indexing engine:
- FunctionIndexer: AST-based scanner that discovers [AzFunction()] functions
  from .ps1/.psm1 files without executing code
- AttributeToBindingConverter: Converts attribute ASTs to BindingInfo and raw
  binding JSON via reflection-based property discovery
- Type accelerators registered for all binding attributes

Attribute classes (30+):
- Triggers: HttpTrigger, TimerTrigger, QueueTrigger, BlobTrigger,
  EventHubTrigger, CosmosDBTrigger, ServiceBusTrigger, EventGridTrigger,
  SignalRTrigger, OrchestrationTrigger, ActivityTrigger, EntityTrigger,
  GenericTrigger
- Inputs: BlobInput, CosmosDBInput, TableInput, SignalRInput, DurableClient,
  GenericInputBinding
- Outputs: HttpOutput, QueueOutput, BlobOutput, CosmosDBOutput, TableOutput,
  EventHubOutput, EventGridOutput, ServiceBusOutput, SignalROutput,
  GenericOutputBinding

Validation:
- Exactly one trigger per function
- Unique binding names within a function
- Required binding properties (e.g. Schedule for TimerTrigger)
- Graceful error handling with per-file skip-and-continue

Runtime integration:
- RequestProcessor wires up indexing on FunctionLoadRequest
- FunctionLoader/DependencyManager path resolution for V2 (app root, not
  per-function subdirectories)
- Pipeline output leak warning in PowerShellManager
- worker.config.json declares worker-indexing capability

.funcignore support:
- Parses .funcignore from app root (glob patterns, comments, blank lines)
- Filters files and directories during recursive scanning
- Supports *, **, and ? glob patterns

Identity-based auth for E2E tests:
- EventHubsHelpers/CosmosDBHelpers use DefaultAzureCredential
- Constants updated for fullyQualifiedNamespace/accountEndpoint

V2 E2E test app (test/E2E/TestFunctionAppV2/):
- HTTP, Timer, Queue, Blob, EventHub, CosmosDB, Durable functions
- Extension bundle v4, external Durable SDK

V2 example app (examples/V2App/):
- HTTP, Timer, Queue trigger examples with .funcignore

Unit tests: 428 passing (90 new)
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.

1 participant