Writing functions
A function is a plain C# class. The one rule is that it only uses IFunctionContext: no HttpClient, DbContext or IConfiguration. The
analyzers enforce this at compile time.
using Microsoft.Kiota.Abstractions;using StayFn.Abstractions;using StayFn.Functions.Client; // the typed function client, generated in every function projectusing StayFn.Ohip.Generated.Rsv.Models;
namespace MyCompany.Functions.Rsv; // last segment names a known area: rsv (or set Area = "rsv")
public sealed record ArrivalsOutput(int Count);
public sealed class ArrivalFunctions{ /// <summary>Count arrivals for a day.</summary> /// <param name="date">Arrival date.</param> // becomes the schema description of "date" /// <param name="roomType">Room type code; all room types when omitted.</param> [OhipFunction("count-arrivals", Visibility = FunctionVisibility.TenantApi, Idempotency = IdempotencyMode.None)] [OhipBudget(3)] // OHIP calls it may make; reserved before it runs public async Task<ArrivalsOutput> CountArrivals(DateOnly date, string? roomType, IFunctionContext ctx) { var enabled = await ctx.Vars.TryGetAsync<bool>("arrivals.enabled"); // tenant/environment/hotel variable var day = new Date(date.Year, date.Month, date.Day); // One method per OHIP operationId; hotelId defaults to the request's hotel. Budgeted, retried and recorded. var found = await ctx.Ohip.Api.Rsv.SearchHotelReservationsAsync( new SearchHotelReservationsRequest { ArrivalStartDate = day, ArrivalEndDate = day, Limit = 200 }, ct: ctx.Cancellation); … }}The signature is the contract. Parameters of any serialisable type form the body, a JSON object of camelCase properties
({"date":"2026-09-26","roomType":"KING"}); non-nullable parameters without a default are required, and XML <param> docs become the
schema descriptions. IFunctionContext, CancellationToken, IOhipClient, IVariables, IFunctionInvoker, IEventPublisher and
ILogger parameters are filled from the invocation context, in any position and each at most once. Methods may be static or synchronous and
may return Task, Task<T>, ValueTask, ValueTask<T> or a value. The classic shape Task<TOut> Run(TIn input, IFunctionContext ctx),
where one record is the whole body, still works unchanged; Binding = FunctionBinding.Arguments makes a single record a named argument
instead ({"address":{…}}).
Call other functions through the generated client: await ctx.Functions.Rsv().CountArrivalsAsync(date, "KING"), or for a record function
ctx.Functions.Rsv().GetReservationAsync("12345678") (small records are flattened) or GetReservationAsync(new GetReservationInput(…)).
A renamed function or a changed input breaks the build. ctx.Functions.InvokeAsync<TIn, TOut>("rsv/…", …) remains for dynamic names.
There are three ways to call OHIP from a function, and all of them go through the same authenticated, rate-limited and recorded pipeline:
- The flat operation layer,
ctx.Ohip.Api.<Module>.<Operation>Async(…). Use this by default. It is generated from the specs with one method per operationId in all 22 modules (Api.Rsv,Api.Crm,Api.Rsvasync, …).- Path parameters are required arguments, and the body is the generated model.
hotelIddefaults to the request’s hotel.- Up to 8 query parameters are named optional arguments. More come as
query: q => …over the generated query object. - Deprecated operations are
[Obsolete]and name their replacement. - The
*asyncmodules poll for you.Start…Asyncreturns theLocationinstead. - Hover text shows the real
METHOD /path.
- Typed helpers such as
ctx.Ohip.Reservations.GetByConfirmationAsyncandctx.Ohip.Profiles.SearchByNameAsync. They are the portable surface: the OPERA 5 adapters implement them, while the flat layer and the Kiota clients throw there. - The Kiota clients,
ctx.Ohip.<Api>.Client.…request-builder chains. They send the same requests as the flat layer. MCPohip.operationshows both forms for every operation.
Other capabilities on ctx: ctx.Events (publish an internal event), ctx.Vars.GetSecretAsync, ctx.Logger (PII-masked),
ctx.Cancellation, ctx.Tenant, ctx.CorrelationId. [OhipFunction] properties control Idempotency (when not set: KeyRequired for
record functions, which makes callers send Idempotency-Key, and Derived for plain-signature functions, where identical calls replay
without a header; or set None, KeyRequired, Derived explicitly), Retry (Default, None, Aggressive), TimeoutSeconds (60),
Visibility (Internal by default; TenantApi or Public to expose it over HTTP) and Schedule (6-field cron).
src/StayFn.Functions.Reference has complete, tested examples.
You get a function, its WireMock-backed tests and a catalog doc stub from the same templates in three ways:
- CLI:
dotnet run --project src/StayFn.Cli -- new function count-arrivals --area rsv --input 'date:DateOnly' --output 'count:int'(one to three simple inputs give a plain signature; more, lists or--operationgive an input record).new handler <name> --event reservation.changedandnew scheduled <name> --schedule "0 0 6 * * *"work the same way. - Claude Code: the repository’s
.mcp.jsonregisters thestayfnMCP server. Ask Claude Code for a function and it usescatalog.search→ohip.operation→function.scaffold→function.validate. Seedocs/authoring-with-claude-code.md. - VS Code: the StayFn extension offers StayFn: New Function, validation, CodeLens ▶ Invoke and the live Invocations view. See
docs/vscode-extension.md.
Test functions with StayFn.Testing.FunctionTestHost, which runs the real OHIP pipeline against WireMock. The generated test files show the
pattern: one happy path and one OHIP-error case.
To load your own function assembly into the host, reference it from StayFn.Host, or point stayfn dev --project <your.csproj> at it
during development.