You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix: align compilers, analyzers and the decompiler with what the API Management gateway accepts - #297
Brings the toolkit in line with what the API Management gateway actually accepts and does. Policies the gateway rejects no longer compile, policies it accepts are no longer blocked or mis-emitted, and decompiled C# builds and compiles back to the same XML.
Changes
Output writing
Expression brackets are matched with the C# lexer, so a bracket inside a string, character literal or comment no longer breaks formatting or throws.
InlinePolicy is always read as rawxml, whether or not it is well-formed XML, and keeps every expression of a value.
Text, comments and CDATA next to child elements are written instead of dropped.
The XML body of a liquid set-body is written as markup, in normalised form, also when a < or & is written as text in it (as in {% if n < 2 %}); other bodies stay text. An @(...) inside that markup is template text and is left alone by expression formatting and by the decompiler, and an HTML entity such as in it is text.
Policy compilers and authoring model
llm-content-safety emits <blocklists> and its streaming window attributes.
llm-token-limit accepts a rate limit together with a quota; token-quota is a long.
check-header values and set-status reason are optional; retry interval is required.
send-request / send-one-way-request require a non-empty URL and method unless mode is copy, and reject a nested authentication-basic.
Adds fail-on-cache-removal-error, authentication-endpoint, cache-response and the send-service-bus-message message attributes.
Renewal periods, including those of api and operation rate limits, and retry count are range-checked.
cors, quota and rate-limit are reported when used more than once in a section.
A policy used in a section that does not offer it is an error, judged by the section the method compiles to (OnError(IOutboundContext context) is on-error); only a document's own methods taking a section context are sections.
if (true), if (false) and an id on an if statement compile to choose.
A null argument, an id with no policy after it, a collection given for a single value, and a collection not written in place (such as a method call) are reported.
A non-string set-variable constant is written as an expression so it keeps its type; value types the gateway rejects are reported: Uri, enums, tuples, collections and arrays other than byte[] and sbyte[].
Implicit numeric conversions of a returned value, and of a helper to the property it is assigned to (an int helper for the longTokenQuota), are written as casts.
A cast to object left around an expanded helper is dropped; the gateway rejects an expression typed object.
Emulator-only callback properties such as RetryConfig.ConditionEvaluator are left out of the policy, so a decompiled retry with an expression condition compiles.
A non-constant context.NamedValue name is reported.
The emulator tolerates the newly optional check-header values and set-status reason, and an existence-only check-header passes for a multi-valued header. Its semantic cache store handlers accept the new optional cacheResponse argument.
Analyzers
Accepts Nullable, Tuple, context.Trace, anonymous types and the new context types.
Reports members of object (ToString, Equals, GetHashCode, GetType) where the gateway rejects them.
Expression return types follow what the gateway accepts as a variable value.
New APIM003: a bare Formatting is ambiguous in the gateway between Newtonsoft.Json and System.Xml and must be written with its namespace.
Helpers of a document or fragment class, including nested classes, are analysed without [Expression] when an expression can reach them. The analysis starts from the sections as the compiler finds them (Inbound, Outbound, Backend, OnError, or the first Fragment method, taking one section context), the config factories and [Expression] methods of every document and helper library in the project, and the helpers that take the expression context or a section context, and follows calls by name: bare, this., through a derived document, or qualified with the document's name directly or through a using alias, also from another document or library. A method nothing reaches, such as a ToString() override, is not analysed. A using alias of Formatting is not reported, since the compiler writes it out in full.
docs/ExpressionHelpers.md describes what the analyzer checks.
Decompiler
Keeps validate-parameters / validate-headers children, nested authentication attributes, set-body markup and the id of a choose.
Keeps the lines of a multi-line attribute expression.
Helpers get valid identifiers, the type their attribute needs (uint, long, decimal) and the using directives their expressions need.
A constant choose condition becomes an if; forms with no C# equivalent are kept as InlinePolicy.
Policies in one directory get distinct class names and fragment ids; --no-validate is honoured; a file given through two options is decompiled once, and a sibling file that is not a policy does not change a fragment's id.
--policy-format (--pf) takes rawxml (default) or xml, as for the compiler: as rawxml a value that begins with an expression is C# as written, so "&" in it is kept; as xml the document is parsed as is. A policy read in the wrong format is reported with a hint naming the option. DecompileOptions.PolicyFormat does the same for the library.
A policy that is not well-formed XML has its expressions restored per text node, so one next to child elements keeps its text and a CDATA section stays one.
Templates
The solution template references the published 1.1.0 packages, the fragment template has its using, and the item templates name real files as primary outputs.
Known limitations
Deliberate choices, kept simple rather than modelling every case the compiler handles:
The analyzer follows helper calls by name, so every overload of a called helper is checked, and using static doesn't qualify a bare name.
The analyzer tells a document from a fragment by the interfaces it implements, not by [Document(Type = …)], and roots the first Fragment method of every part of a partial class, where the compiler compiles the attributed part only.
A helper in a class that is neither a document, nested in one, derived from one nor marked [Expression] still needs [Expression] to be checked; a helper inherited from a base class that isn't a document isn't checked.
The compiler compiles the first Fragment method taking a section context and ignores a second overload, as before.
The decompiler writes a file before validating its syntax, so the file can be inspected when validation fails; the exit code is 1.
The decompiler drops the <value> children of a set-header or set-query-parameter whose exists-action is delete; they have no effect.
Breaking changes
IProduct.SubscriptionLimit is renamed SubscriptionsLimit; IUrl.Port is an int.
RetryConfig.Interval is required; CorsConfig.TerminateUnmatchedRequest is a bool?; TokenLimitConfig.TokenQuota is a long?.
The semantic cache store methods take an optional cacheResponse.
AuthenticationManageIdentityReturnValueCompiler is removed.
context.Product, context.Backend and context.Workspace are nullable, and null by default in the emulator.
A config property set to a typed null (default(string), (string?)null, a null constant) is skipped instead of emitted as an empty attribute.
A null policy argument, send-request without a URL or method, a policy in a section that does not offer it, a config collection not written in place, and a set-variable value typed string[] or as a collection no longer compile.
The analyzers ship with the Authoring package, so projects with document helpers not marked [Expression] can now get APIM001–APIM003 errors for code the gateway rejects.
The decompiler reads a policy as rawxml by default. A policy whose expressions are XML-escaped needs --policy-format xml (or DecompileOptions.PolicyFormat = PolicyFormat.Xml), where it was read correctly without an option before.
Testing
Unit tests: Core 968, Decompiling 191 (2 skipped), Analyzers 91, Testing 303 on .NET 8 and .NET 10. .NET 9 was not run. The round-trip tests build the decompiled C# with full references, and a rawxml round trip of what the compiler writes is compared byte for byte.
Verified against a live API Management instance (Developer SKU):
save-time acceptance of the compiled policies;
runtime behaviour through the gateway where the result matters;
a round trip of 162 gateway-accepted policies: decompile, build, recompile, redeploy.
Re-verified on 2026-10-07 against a second Developer instance: 581 save-time cases, 43 runtime cases and a 159-policy round trip. That gateway enforces the documented set-variable value types (string[] and List<string> rejected), which the 2026-10-05 one did not; the compiler follows the documented list.
- expression brackets are matched with the C# lexer, so a bracket in a string, a
character literal or a comment no longer breaks formatting or throws
- InlinePolicy is read as rawxml whether or not it is well-formed XML, keeps every
expression of a value and keeps a comment at the start of an expression
- text, comments and CDATA next to child elements are written instead of dropped
- the XML body of a liquid set-body is written as markup; other bodies stay text.
Checked with requests through the gateway: a body without a template written as
markup comes back empty, a liquid template written as escaped text comes back escaped
- bin and obj folders are only excluded below the source root, and a rooted document
name stays inside the output folder
…gement gateway
Every change was checked by saving the compiled policy to a gateway, and by calling it
where the result matters.
Policies
- llm-content-safety emits <blocklists> and its streaming window attributes
- llm-token-limit accepts a rate limit together with a quota; token-quota is a long
- check-header values are optional; set-status reason is optional; retry interval is required
- send-request and send-one-way-request need a URL and a method unless mode is copy,
and reject a nested authentication-basic
- add fail-on-cache-removal-error, authentication-endpoint, cache-response and the
send-service-bus-message message attributes
- renewal periods and the retry count are checked against the accepted range
- cors, quota and rate-limit are reported when used more than once in a section
- cors terminate-unmatched-request is a bool; ForwardRequest accepts a config factory
Sections and statements
- a policy used in a section whose context does not offer it is an error, also when the
C# is not compiled first
- only a document's own methods taking the section context are sections
- if (true), if (false) and an id on an if statement compile to choose
- an id that no policy follows, a null argument and a collection given for a single
value are reported instead of being emitted or dropped
Expressions
- a set-variable constant that is not a string is written as an expression so it keeps
its type; an expression of a type the gateway rejects as a variable is reported
- an implicit numeric conversion of a returned value is written out as a cast
- a non-constant context.NamedValue name is reported
- add context.Backend, context.Workspace, Deployment.Gateway, SustainabilityInfo,
Request.Foundry, byte[] Encrypt and Decrypt, VerifyNoRevocation and Url.ToString;
Product.SubscriptionsLimit and an int Url.Port replace the names the gateway rejects
Also fixes crashes on non-inline collections, copy-pasted policy names in diagnostics
and XML docs that contradicted the gateway.
BREAKING CHANGE: IProduct.SubscriptionLimit is SubscriptionsLimit, IUrl.Port is an int,
RetryConfig.Interval is required, CorsConfig.TerminateUnmatchedRequest is a bool,
TokenLimitConfig.TokenQuota is a long, the semantic cache store methods take an optional
cacheResponse, and AuthenticationManageIdentityReturnValueCompiler is removed.
context.Product, context.Backend and context.Workspace are nullable and null by default in
the emulator, as they are in the gateway without a subscription, a selected backend
entity or a workspace. A null
policy argument, send-request without a URL or method, and a policy in a section that
does not offer it no longer compile.
- Nullable, Tuple, context.Trace, ProductState, Authorization, anonymous types and the
new context types are accepted
- members of object (ToString, Equals, GetHashCode, GetType) are reported: the gateway
rejects them on a value typed object or on a type that does not declare them
- the allowed return types of an expression method follow the types the gateway
accepts as a variable value; of arrays only string[] and byte[]
- APIM003 reports a type name the gateway cannot resolve on its own: Formatting exists
in both Newtonsoft.Json and System.Xml and has to be written with its namespace
- the helpers of a policy document or fragment, including those in a nested class, are
analysed without [Expression], as the compiler turns them into policy expressions
either way
BREAKING CHANGE: the analyzers ship with the Authoring package, so a project whose
document helpers were not marked [Expression] can now get APIM001, APIM002 or APIM003
errors for code the gateway rejects when the policy is saved.
…me XML
Checked by decompiling 162 policies the gateway had accepted, building the C# in one
project, compiling it back and saving the result to the gateway again.
- keep validate-parameters and validate-headers children, nested authentication
attributes, the markup of a set-body and the id of a choose
- keep the lines of a multi-line attribute expression, so a line comment no longer
swallows the code after it
- a value with several named values or several expressions stays a string
- helpers get valid identifiers, the type their attribute needs (uint, long, decimal)
and the using directives their expressions need
- a constant choose condition becomes an if statement; a form that has no C#
equivalent is kept as InlinePolicy with its expressions written raw
- policies in one directory get distinct class names and fragment ids, and
--no-validate is honoured
The solution template references the published 1.1.0 packages instead of commented-out
references to a version that was never published, the fragment template has its using
directive, and the item templates name real files as primary outputs.
… bodies and check-header
- a method is only a section when it takes a section context, so an overload such as
Inbound(int) no longer becomes another section
- a set-variable value written in place, such as an enum member, is reported when the
gateway rejects its type, as a value returned by a helper already was
- a liquid set-body that is well-formed XML is written as markup also when writing it
back changes its form, such as single-quoted attributes; it was written as escaped text
- the emulator passes an existence-only check-header for a header with several values
The new null handling only recognizes the untyped default literal. Typed null constants such as CachingType = default(string) or a const string whose value is null continue through ProcessExpression and are emitted as policy values instead of being treated as unset; the same form can also satisfy a required-property presence check incorrectly. Use the semantic constant value for every expression, not only DefaultLiteralExpressionSyntax.
…ons and query expressions
- a section context is recognised by its type in the authoring library, so an alias
of it is a section and an unrelated type with the same name is not
- a collection of a policy configuration that is not written in place, such as a
method call, is reported instead of being emitted without its items
- a decompiled query expression gets the System.Linq using directive
This exempts every mode other than the exact literal "new", not just "copy". Consequently Mode = "typo" with no URL or method produces no required-field diagnostics and emits a policy the gateway rejects. Only a literal copy (or a genuinely nonconstant expression whose result cannot be known here) should bypass these checks; other literal modes should be rejected.
Use long handler for TokenQuota named values and expressions
The non-expression branch still uses the int handler even though TokenQuota is now long. In particular, a valid named-value token such as token-quota="{{quota}}" generates an int-returning helper/cast, so values above Int32.MaxValue fail and the round trip no longer preserves the required type. Add a long-specific value handler and use it for literals, expressions, and named-value tokens.
Restore placeholders in mixed-content XML text nodes
src/Core/Decompiling/PolicyDecompiler.cs:266
Raw expressions in mixed content can remain as preprocessing placeholders. RestorePlaceholders only restores element.Value when !element.HasElements, so rawxml such as a liquid set-body with expression text followed by a child element keeps __APIM_EXPR_n__ and decompiles that marker instead of the expression. Restore placeholders in each XText node regardless of whether its parent also has elements.
…ected policy's id
- a set-variable helper that returns the result of another helper is followed, so a
value of a type the gateway rejects is reported however many helpers wrap it
- a null written as default(string), (string?)null or a null constant is treated as
null: reported as a policy argument and skipped as a config property, instead of
being emitted as an empty value
- the id of a policy rejected for its section no longer moves on to the next policy
…d value types
- default and a cast of null are recognised as null for a nullable value type, so an
optional argument such as the cacheResponse of cache-store is left out for them
as it is for a literal null
- an implicit numeric conversion of a returned value is written as a cast also when
the helper returns a nullable numeric type
- the section-context check of a document reuses the one of the expression compiler
- the emulator handlers of llm-semantic-cache-store and azure-openai-semantic-cache-store
accept the optional cacheResponse argument; every call threw since it was added
- a score-threshold put together from a named value and text is kept as written instead
of being decompiled to a helper for a named value with the wrong name
- an sbyte[] expression value is reported: only string[] and byte[] were accepted when
saved to a gateway
- an id that is still waiting for its policy when another WithId is given is reported,
instead of one of the two ids being dropped
- a config property that holds an emulator-only callback, such as the ConditionEvaluator
the decompiler writes for a retry with an expression condition, is left out of the
policy instead of being reported as an unsupported parameter
- a helper returning (object)new { a = 1 } hid the anonymous type from the value check,
which saw only object; a cast to object and parentheses are now looked through, as a
call to another helper already was
- the cast to object that expanding a helper declared to return object leaves around
its result is dropped: the gateway types an expression by its code and rejects one
typed object
- a helper whose numeric type C# converts to the type of the property it is assigned to,
such as an int helper for the long TokenQuota, has that conversion written out: the
gateway rejects an int expression for token-quota
- the unit-test compilation references System.Runtime, so the types of the authoring
library resolve in it as they do in a real project
Preserve markup in root value elements during decompilation
src/Core/Decompiling/PolicyDecompiler.cs:280
A direct <value> child is not always the toolkit's text wrapper; it can be the root element of a legitimate liquid XML body. For <set-body template="liquid"><value><a>1</a></value></set-body>, this path treats it as non-markup, and SetBodyDecompiler then takes the wrapper branch and flattens the content to 1, dropping both elements on round-trip. Distinguish a text-only wrapper from a <value> element that itself contains markup in both places.
Reject mixed text and markup when selecting XML output
src/Core/Serialization/RawXmlContent.cs:53
Wrapping the value makes mixed non-XML content parse successfully whenever it contains an element. For example, the liquid JSON body { "a": "<b />" } produces top-level text plus a <b> node, so this classifies it as XML markup even though non-XML bodies must stay text. Require the parsed top-level content to contain no non-whitespace text before selecting markup output.
…y or not known
Any mode other than new skipped the check, so a mistyped mode compiled without a URL
or method. Only the literal copy, an expression or a named value skip it now.
Falling back to plain text changes these values to strings, contradicting the typed-value handling above. double.NaN and the infinity members are valid policy expressions, so SetVariable("v", double.NaN) should emit @(double.NaN) (and likewise for float/infinities) rather than value="NaN".
The gateway treats an @(...) inside that markup as template text, not as a policy
expression.
- formatting expressions leaves the markup alone; reflowing it as C# changed the body
- the decompiler keeps the markup as XML instead of writing its @(...) spans raw, which
left a string that was no longer well-formed and compiled back to an escaped body
…th < or &
A template such as {% if item.Count < 2 %}<p>...</p>{% endif %} is not well-formed XML,
so the whole body was written as escaped text, which the gateway returns still escaped.
A < or & that does not start a tag or an entity is now read as text. It is written
escaped, the form the gateway accepts inside a liquid tag: {% if 1 < 2 %} was checked
with a request through a gateway.
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🟡 Changes recommended
Liquid-body serialization and duplicate-policy counting can alter valid templates or report false errors, and implicit char conversions lose their declared type.
Exclude liquid set-body markup from descendant policy counts
src/Core/Compiling/DocumentCompiler.cs:120
This descendant count also includes XML markup inside a liquid set-body. A template such as <rate-limit /> plus one real context.RateLimit(...) is therefore reported as two policies even though the first element is body content, not an APIM policy. Exclude descendants of liquid set-body elements (and keep counting policies nested under choose/wrapper policies).
Preserve char types in implicit numeric constant conversions
char is also a target of implicit numeric constant conversions. For example, char Separator(IExpressionContext context) => 65; currently lowers to @(65), so JsonToXmlConfig.NamespaceSeparator reaches the gateway as an int rather than the helper's declared char type. Include System_Char so this path emits @((char)65); this also preserves char? helper returns.
…s and the decompilers
Compiler
- every value a multi-statement helper returns is converted to the numeric type of the
property it is assigned to, as a single-expression helper already was, also when the
helper reads a raw named value; a conversion of a raw named value covers all of its text
- cors, quota and rate-limit used more than once are reported in a fragment too, and an
element named like a policy in the markup of a liquid body is content, not a policy
- an id does not move past any statement that is not a policy
- the section-context check of a document reuses the one of the expression compiler
Liquid bodies
- a liquid set-body is code only when it is one whole expression; one that merely starts
with @( is a template
- a CDATA section in a liquid body is markup and stays one; the markup is left alone when
expressions are formatted, and a CDATA section or a comment in it keeps its text when a
< or & written as text is escaped
- a body given through a value element is a policy value only when the value element is
all there is in it; a template with one among other content is markup
- the decompiler keeps the markup of a liquid body as XML, with its whitespace, and a
score-threshold put together from a named value and text as written
Decompiled C#
- the using directives cover the remaining types the analyzer allows, the certificate
extension methods, a query expression, and Authorization next to a System.Net type
- a token-quota named value gets a helper that returns a long
Tests
- the unit-test compilation references System.Runtime, so the types of the authoring
library resolve as in a real project; three tests that passed on any diagnostic check
for the one they are about; two sample policies returned a Guid where a string is needed
- the TokenQuotaPeriod and CarbonIntensityCategory comments say what the code does
Every method of a document that returns a value was analysed as expression code, which
reported a ToString() override or a static factory that never becomes a policy
expression. A method is now analysed when it takes the expression context or is called,
directly or through other helpers, from a section, a configuration factory or such a
method of the same document or of a document derived from it, which is when the
compiler expands it. Calls are matched by name: Helper(), this.Helper(), base.Helper(),
a cast of this, and a receiver made of the document's namespace, its types, the classes
it is nested in or a using alias.
APIM003 does not report a using alias of Formatting: the compiler writes it out as the
name it stands for, which the gateway accepts. The helper guide says which methods are
analysed and which named value helpers compile to the bare token.
… compiler writes them
--policy-format (--pf) takes rawxml, the default, or xml, like the compiler, and so does
DecompileOptions.PolicyFormat. A rawxml value that begins with an expression is C# as it
is written, so an entity in it, such as the string "&", is kept; everything else is
XML, an @(...) further into a value or in the markup of a liquid body included. As xml the
document has to be well-formed and is parsed as it is. Before, the decompiler read a
well-formed document as xml, which turned Method + "&" into Method + "&", and guessed
at the rest by whether an expression looked escaped.
When a policy is read in the wrong format the decompiler says so: a rawxml policy read as
xml fails to parse, an xml policy read as rawxml leaves C# that does not parse, and both
messages name the option.
Also: expressions of a policy that is not well-formed XML are restored per text node, so
one next to child elements no longer keeps its placeholder and a CDATA section stays one;
a character reference is decoded, unless it is no character XML allows; the decompiler
tool counts only policy documents when it decides whether a fragment shares its
directory, and decompiles a file found through two options once; the round-trip tests
build the decompiled C# with full references.
BREAKING CHANGE: the decompiler reads a policy as rawxml by default. A policy whose
expressions are XML-escaped needs --policy-format xml (or DecompileOptions.PolicyFormat
= PolicyFormat.Xml), where it was read correctly without an option before.
…nd analyse every value helper
A void method of a document that takes no section context is not a section and nothing is
compiled from it, so a helper only it calls is not a policy expression and is left alone.
Reachability is worked out once per compilation over every document and expression helper
library: a configuration factory is a root wherever it is used from, and code of another
document or library reaches a helper it names, Shared.Helper(), directly, through its own
helpers or through a using alias. A helper is told from a section by its return type,
which approximates what the compiler does, so one that takes a section context is analysed
too; reading the ExpressionContext of an authoring section context is the gateway's context
and is allowed. A section that implements IDocument explicitly is a root, and a parameter
typed IHaveExpressionContext itself counts as a section context.
… compiles
A void method taking a section context was a root whatever its name and
however many parameters it had, so the helpers that only an uncompiled
method such as Audit(IInboundContext context) or
Inbound(IInboundContext context, int retries) calls were analysed. A void
root now has to be a section as DocumentCompiler finds it: a method of a
document named Inbound, Outbound, Backend or OnError, or of a fragment
named Fragment, taking one section context, implemented explicitly or
not. Value-returning helpers taking a section context or the expression
context stay roots.
…ly, as the compiler compiles
DocumentCompiler compiles the first Fragment method taking one section
context of the class declaration it compiles and leaves an overload taking
another, so the analyzer no longer roots such an overload: the helpers
only it calls were reported for code that is never compiled. The members
of a partial class come in file order, so the first Fragment method of
each part is taken rather than of the class.
…periods, empty send-request values and tag names
A policy's availability is checked against the context of the section
its method compiles to, OnError(IOutboundContext context) is on-error,
not against the context the method is written with. The renewal period
of an api or operation rate limit has the same 1-300 range as the
policy's own. An empty or blank send-request URL or method is reported
as missing. A tag in a liquid body may start with a letter outside ASCII
or a colon, as well as an underscore.
WindowSize is documented as valid only from 200 through 1000, but this emits any literal unchanged, so values such as 199 or 1001 compile into policies the gateway rejects. Apply the existing literal range diagnostic before adding the attribute; expression values can remain unchecked.
A policy has no DTD, so the only entities are XML's five and character
references. The stray-character pass exempted any entity-shaped name, so
a liquid body with failed to parse as markup and was written as
escaped text, which the gateway returns escaped. It is now escaped to
&nbsp; inside the markup, as a bare & already was.
The analyzer's configuration factory is a method returning a record of
the authoring namespace, not anything of that namespace. The docs note
that a factory may take IHaveExpressionContext.
…compiles them included
A section returns nothing and takes one of the five section contexts;
IHaveExpressionContext, which they share, isn't one, so a method taking
it is no longer compiled as a second section nor rooted by the analyzer,
and a value-returning method taking a section context is a helper, as
the analyzer already had it. A set-variable value of a nullable type is
as rejected as its value, and a helper declared with a rejected type
returns one whatever its body returns. The name of a named value has one
definition, shared by the decompiler's token patterns and the formatter,
and a token in an attribute is a named value on the same terms as one in
a string. [NamedValue] is recognised the same way wherever it is looked
for: by its type, or by its name alone when the type can't be resolved.
The decompiler keeps whitespace in a body written as markup, liquid or
not. The entities a policy may hold are defined once for the writer and
the decompiler.
…y or collection
The gateway's list of value types has byte[] and sbyte[] as its only
arrays and no collection. A Developer instance probed on 2026-10-07
rejects string[], List<string>, IEnumerable<string>, HashSet<string> and
byte?[] with "Expression return type ... is not allowed", where the
instance probed on 2026-10-05 had accepted the first three; the
documented list is what the stricter gateway enforces. The compiler
reports such a value, every type of System.Collections and System.Linq
included, and the analyzer such a return type. A collection declared
elsewhere, such as a user's own, isn't reported, as the gateway hasn't
been seen to reject it.
The new encryption/decryption emulation has no automated coverage, despite introducing six overloads and algorithm-name dispatch. Add tests for AES and TripleDES round trips through both the named-algorithm and SymmetricAlgorithm overloads, plus the unsupported-algorithm path, so gateway-expression emulation regressions are caught.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Brings the toolkit in line with what the API Management gateway actually accepts and does. Policies the gateway rejects no longer compile, policies it accepts are no longer blocked or mis-emitted, and decompiled C# builds and compiles back to the same XML.
Changes
Output writing
InlinePolicyis always read as rawxml, whether or not it is well-formed XML, and keeps every expression of a value.set-bodyis written as markup, in normalised form, also when a<or&is written as text in it (as in{% if n < 2 %}); other bodies stay text. An@(...)inside that markup is template text and is left alone by expression formatting and by the decompiler, and an HTML entity such as in it is text.Policy compilers and authoring model
llm-content-safetyemits<blocklists>and its streaming window attributes.llm-token-limitaccepts a rate limit together with a quota;token-quotais along.check-headervalues andset-statusreason are optional;retryinterval is required.send-request/send-one-way-requestrequire a non-empty URL and method unless mode iscopy, and reject a nestedauthentication-basic.fail-on-cache-removal-error,authentication-endpoint,cache-responseand thesend-service-bus-messagemessage attributes.apiandoperationrate limits, and retry count are range-checked.cors,quotaandrate-limitare reported when used more than once in a section.OnError(IOutboundContext context)ison-error); only a document's own methods taking a section context are sections.if (true),if (false)and an id on anifstatement compile tochoose.set-variableconstant is written as an expression so it keeps its type; value types the gateway rejects are reported:Uri, enums, tuples, collections and arrays other thanbyte[]andsbyte[].inthelper for thelongTokenQuota), are written as casts.objectleft around an expanded helper is dropped; the gateway rejects an expression typedobject.RetryConfig.ConditionEvaluatorare left out of the policy, so a decompiledretrywith an expression condition compiles.context.NamedValuename is reported.context.Backend,context.Workspace,Deployment.Gateway,SustainabilityInfo,Request.Foundry,byte[]Encrypt/Decrypt,VerifyNoRevocation,Url.ToString().check-headervalues andset-statusreason, and an existence-onlycheck-headerpasses for a multi-valued header. Its semantic cache store handlers accept the new optionalcacheResponseargument.Analyzers
Nullable,Tuple,context.Trace, anonymous types and the new context types.object(ToString,Equals,GetHashCode,GetType) where the gateway rejects them.APIM003: a bareFormattingis ambiguous in the gateway betweenNewtonsoft.JsonandSystem.Xmland must be written with its namespace.[Expression]when an expression can reach them. The analysis starts from the sections as the compiler finds them (Inbound,Outbound,Backend,OnError, or the firstFragmentmethod, taking one section context), the config factories and[Expression]methods of every document and helper library in the project, and the helpers that take the expression context or a section context, and follows calls by name: bare,this., through a derived document, or qualified with the document's name directly or through ausingalias, also from another document or library. A method nothing reaches, such as aToString()override, is not analysed. Ausingalias ofFormattingis not reported, since the compiler writes it out in full.docs/ExpressionHelpers.mddescribes what the analyzer checks.Decompiler
validate-parameters/validate-headerschildren, nested authentication attributes,set-bodymarkup and the id of achoose.uint,long,decimal) and theusingdirectives their expressions need.choosecondition becomes anif; forms with no C# equivalent are kept asInlinePolicy.--no-validateis honoured; a file given through two options is decompiled once, and a sibling file that is not a policy does not change a fragment's id.--policy-format(--pf) takesrawxml(default) orxml, as for the compiler: asrawxmla value that begins with an expression is C# as written, so"&"in it is kept; asxmlthe document is parsed as is. A policy read in the wrong format is reported with a hint naming the option.DecompileOptions.PolicyFormatdoes the same for the library.Templates
using, and the item templates name real files as primary outputs.Known limitations
Deliberate choices, kept simple rather than modelling every case the compiler handles:
using staticdoesn't qualify a bare name.[Document(Type = …)], and roots the firstFragmentmethod of every part of a partial class, where the compiler compiles the attributed part only.[Expression]still needs[Expression]to be checked; a helper inherited from a base class that isn't a document isn't checked.Fragmentmethod taking a section context and ignores a second overload, as before.<value>children of aset-headerorset-query-parameterwhoseexists-actionisdelete; they have no effect.Breaking changes
IProduct.SubscriptionLimitis renamedSubscriptionsLimit;IUrl.Portis anint.RetryConfig.Intervalis required;CorsConfig.TerminateUnmatchedRequestis abool?;TokenLimitConfig.TokenQuotais along?.cacheResponse.AuthenticationManageIdentityReturnValueCompileris removed.context.Product,context.Backendandcontext.Workspaceare nullable, and null by default in the emulator.default(string),(string?)null, a null constant) is skipped instead of emitted as an empty attribute.send-requestwithout a URL or method, a policy in a section that does not offer it, a config collection not written in place, and aset-variablevalue typedstring[]or as a collection no longer compile.[Expression]can now getAPIM001–APIM003errors for code the gateway rejects.rawxmlby default. A policy whose expressions are XML-escaped needs--policy-format xml(orDecompileOptions.PolicyFormat = PolicyFormat.Xml), where it was read correctly without an option before.Testing
set-variablevalue types (string[]andList<string>rejected), which the 2026-10-05 one did not; the compiler follows the documented list.