Analyzer rules
StayFn.Analyzers is a Roslyn analyzer (netstandard2.0, packaged under analyzers/dotnet/cs, referenced as an analyzer by
StayFn.Functions.Reference) that turns the design rules for function code into build errors. It only looks at classes that carry
[OhipFunction] or an [OhipFunction]/[EventHandler] method; infrastructure code in the same assembly is not inspected. Types are
matched by metadata name, so the analyzer needs no reference to StayFn, Entity Framework or Microsoft.Extensions packages.
All rules are Error by default. Known areas are the generated OHIP module codes plus custom (StayFnDiagnostics.KnownAreas,
pinned to StayFn.Abstractions.KnownAreas.All by a test).
Known limits: a forbidden type reached through a static member of another class, through object, or through reflection is not seen;
the analyzer is a guard rail for authors and the runtime registry stays the gate at start-up. Code fixes (“add IFunctionContext
parameter”, “replace HttpClient with ctx.Ohip”, “make the input a record”) are listed below.
Tests: tests/StayFn.AnalyzerTests (Microsoft.CodeAnalysis.CSharp.Analyzer.Testing).
Code fixes
Section titled “Code fixes”StayFn.Analyzers.CodeFixes ships in the same package (analyzers/dotnet/cs, next to the analyzer) and holds three code fixes, two of them offered as light-bulb
actions in Visual Studio, Rider and VS Code (C# Dev Kit / OmniSharp). It is a separate assembly because code fixes need Roslyn
Workspaces, which the command-line compiler does not load (RS1038); StayFn.Functions.Reference references both as analyzers. Every
fix supports Fix All (document, project, solution).
| Fix | Diagnostic | What it does | Not offered when |
|---|---|---|---|
| Add IFunctionContext parameter | STAYFN004 | Kept in the assembly but no longer offered: a function or handler without a context is valid since plain signatures, so no remaining STAYFN004 is about a missing context | Always (it registers no action) |
| Replace HttpClient usage with ctx.Ohip | STAYFN001 | Rewrites SendAsync(request) to ctx.Ohip.RawAsync(request, …) and GetAsync/DeleteAsync(uri), PostAsync/PutAsync/PatchAsync(uri, content) to ctx.Ohip.RawAsync(new HttpRequestMessage(HttpMethod.X, uri) { Content = content }, …), keeping the caller’s cancellation token or using ctx.Cancellation. The URI is kept as written; RawAsync sends it relative to the OHIP gateway, so check it is an OHIP path. Delete the now-unused client yourself |
The call returns something else (GetStringAsync, GetStreamAsync, …), passes an HttpCompletionOption, or the enclosing method has no IFunctionContext parameter |
| Make input type a record | STAYFN005 | Turns the input (or event) class into a sealed record: a class made only of public auto-properties (and constructors) becomes public sealed record X(T1 A, T2 B); keeping its doc comment; any other class keeps its body, loses abstract and gets public constructors |
The type is an interface, a record, static, partial, has a base class or derived classes, or the diagnostic is on an output |
Typed function client (source generator)
Section titled “Typed function client (source generator)”The same package ships FunctionClientGenerator, an incremental source generator, so every project that references StayFn.Analyzers
(as StayFn.Functions.Reference does, or through the NuGet package) gets typed function-to-function calls with no new reference:
using StayFn.Functions.Client;
var result = await ctx.Functions.Rsv().SyncReservationToCrmStubAsync(new SyncReservationInput(evt.ConfirmationNumber), ctx.Cancellation);- It reads every
[OhipFunction]of the compilation and of every referenced assembly that referencesStayFn.Abstractions, from metadata. Areas resolve as in the registry (attribute, class[FunctionArea], assembly[FunctionArea("…")], known namespace suffix,customoutside shipping assemblies). - It writes
StayFn.Functions.Client.g.cs:FunctionAreaswith one extension method per area onIFunctionInvoker(Rsv(),Crm(), …) and onereadonly struct <Area>Functionsper area with a<Name>Asyncmethod per function. Each method callsIFunctionInvoker.InvokeAsync<TIn, TOut>("<area>/<name>", …)with the full name as a literal, so the child invocation, its budget and its idempotency are exactly those of the string call. The description (or the XML<summary>) becomes the method’s<summary>. - Overloads: every record function has the record overload (
GetReservationAsync(GetReservationInput input, CancellationToken ct = default)); a positional record of at most eight parameters also gets a flattened one (GetReservationAsync(string confirmationNumber, …), defaults kept). A plain-signature (Arguments-mode) function gets its body parameter list (CountArrivalsAsync(DateOnly date, string? roomType, …)) and sends a generated payload whose properties serialise to the same camelCase names as the registry’s schema. - The types are
internal: each function project has its own copy, so two libraries that both run the generator never collide. - Functions the analyzer rejects (bad names or areas, a shipping function without an area, invalid shapes) and functions whose input or output type is not accessible from the project are skipped without a diagnostic; STAYFN004/005/006 already report them.
ctx.Functions.InvokeAsync("…")stays for dynamic names. MCPfunction.validateruns the generator as a real build does.