Skip to content

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.

Id Rule Fix
STAYFN001 System.Net.Http.HttpClient or IHttpClientFactory declared (field, property, parameter, local), created or called inside a function class Call OHIP through ctx.Ohip (typed helpers, generated module clients, or RawAsync as the escape hatch) so tokens, rate limits, retries and call attribution stay under the runtime
STAYFN002 Microsoft.EntityFrameworkCore.DbContext (any derived type, including StayFnDbContext) declared, created or called inside a function class Functions are stateless: keep state in variables (ctx.Vars), in OHIP, or publish an event (ctx.Events) for a runtime component to persist
STAYFN003 Microsoft.Extensions.Configuration.IConfiguration declared or indexed inside a function class Read tenant/hotel scoped settings through ctx.Vars.GetAsync<T> / TryGetAsync<T> / GetSecretAsync
STAYFN004 A function or handler method has an invalid shape: void return or a task type other than Task/Task<T>/ValueTask/ValueTask<T>; a handler returning a value or taking other than one event parameter; a context-bound type (IFunctionContext, CancellationToken, IOhipClient, IVariables, IFunctionInvoker, IEventPublisher, ILogger) twice; body parameters whose names collide after camelCase (Date, date); Binding = FunctionBinding.Record on anything but one class/record parameter; ref/out/in parameters; a generic method; a class-level [OhipFunction] without exactly one public instance method Any number of typed body parameters, the context-bound types at most once each and in any position, a task or plain return value; static methods are fine. Task<TOut> Run(TIn input, IFunctionContext ctx) is still valid (Record mode)
STAYFN005 A body parameter (each one, in either binding mode), the output (Task<TOut>, ValueTask<TOut> or a plain return type) or an event type is not serialisable with System.Text.Json: Stream, Task, delegates, HttpRequestMessage/HttpResponseMessage, Type, object, interfaces, abstract classes, types without a public constructor Use simple types, records or POCOs with a public constructor; the same predicate runs in FunctionRegistry at start-up
STAYFN006 In a shipping assembly ([assembly: FunctionArea] marker or StayFn.Functions.Reference) an [OhipFunction] resolves no area Set Area = "…" on the attribute, add [FunctionArea("…")] to the class (or a value on the assembly attribute), or place the class in a namespace whose last segment is a known area (…Reference.Rsv)

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).

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

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 references StayFn.Abstractions, from metadata. Areas resolve as in the registry (attribute, class [FunctionArea], assembly [FunctionArea("…")], known namespace suffix, custom outside shipping assemblies).
  • It writes StayFn.Functions.Client.g.cs: FunctionAreas with one extension method per area on IFunctionInvoker (Rsv(), Crm(), …) and one readonly struct <Area>Functions per area with a <Name>Async method per function. Each method calls IFunctionInvoker.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. MCP function.validate runs the generator as a real build does.