Skip to content

Write a function

Install Ankus.Tool from nuget.org, then create a solution:

Terminal window
dotnet tool install --global Ankus.Tool --version 0.2.1
ankus new Hello
cd Hello

The solution includes an extension project, managed tests, and tests that load the native extension into PostgreSQL.

Alternatively, install the matching .NET template package:

Terminal window
dotnet new install Ankus.Templates@0.2.1
dotnet new ankus -n Hello
cd Hello
dotnet tool restore

Both paths include the same managed and backend tests, SQL regression setup and matching local tool manifest. After dotnet tool restore, run extension commands with dotnet ankus. Use dotnet new ankus-worker -n MyWorker for a background worker with shared preload and a test that checks its separate PostgreSQL process.

MSTest is the default framework. Add --test-framework xunit or --test-framework nunit to either creation command to use that framework’s managed tests, backend case discovery and asynchronous fixture cleanup.

The templates derive a valid SQL identifier from the project name. For example, dotnet new ankus -n 1Ext uses _1_ext as the extension name. Generated regression results are ignored by Git; authored SQL and expected results remain tracked.

The generated src/Hello/Hello.csproj uses Ankus.Sdk:

<Project Sdk="Ankus.Sdk">
<PropertyGroup>
<AnkusExtensionName>hello</AnkusExtensionName>
<AnkusExtensionVersion>0.1.0</AnkusExtensionVersion>
<AssemblyName>hello</AssemblyName>
</PropertyGroup>
</Project>

The SDK version is pinned in the solution’s global.json, and shared settings such as the target framework and nullable annotations are in Directory.Build.props.

The SDK enables Native AOT and includes matching runtime and source-generator packages. It works with ordinary NuGet configuration and Central Package Management.

Edit src/Hello/Functions.cs and mark a synchronous static method with [PgFunction]:

using Ankus;
public static class Functions
{
[PgFunction]
public static string Greet(string name) => $"Hello, {name}!";
}

The method is exposed as greet(text). Names use snake case by default. Ankus generates the native entry point and SQL declaration during publishing.

C# parameter names also use snake case in SQL, and optional arguments become SQL defaults. See function declarations for named calls, schema placement, and PostgreSQL execution options.

See enumerated types for custom C# enums, labels and type dependencies.

SQL entry methods must be synchronous, concrete static methods. The method and every containing type must be accessible from generated code in the extension assembly: use public or internal, or protected internal for members and nested types. protected, private protected and private do not grant that assembly access. Generic methods, generic containing types and file-local types cannot provide the concrete call required by the generated Native AOT dispatcher.

Pass parameters and return values by value. PostgreSQL supplies SQL datums, not C# ref, in, out or ref readonly slots. Injected PgMemoryContext and PgFunctionContext parameters also use by-value passing. At most 100 SQL arguments are allowed; injected contexts do not count toward that limit.

Use a supported type, a custom type or an explicit datum mapping for inputs and results. params T[] requires a supported SQL array conversion. params byte[] is invalid because byte[] maps to scalar bytea; use params byte[][] for variadic binary values. Ankus does not adapt span-based params collections.

Invalid signatures report a specific error at the offending declaration:

Diagnostic Required correction
ANKUS033 Declare a static entry method.
ANKUS034 Make the method and containing types accessible within the extension assembly: public, internal, or protected internal where permitted.
ANKUS035 Use a non-generic entry method in non-generic containing types.
ANKUS036 Put the entry attribute on a concrete implementation.
ANKUS037 Return the SQL value by value.
ANKUS038 Pass arguments and injected contexts by value.
ANKUS039 Give the result a supported SQL conversion.
ANKUS040 Give the parameter a supported SQL conversion.
ANKUS041 Use a supported SQL array for the variadic parameter.
ANKUS042 Reduce the number of SQL arguments to 100 or fewer.
ANKUS043 Move the method out of a file-local type.

Editors offer fixes for ANKUS033, ANKUS034, ANKUS038 and ANKUS043. Make static appears when the method uses no instance state and no caller invokes it through an instance. Make internal replaces a private or protected accessibility on the method, or on a containing type and each of its partial declarations, and replaces file on a containing type when no other type shares its name. Pass by value removes in or ref readonly from the parameter and from callers’ arguments, and leaves the source unchanged if a delegate, overload or interface still needs the reference. ref and out get no fix, because the caller would no longer see the method’s writes. Remove deletes an attribute or option that cannot apply where it is written: Conditional on any entry, or UnmanagedCallersOnly on an initializer, worker, callback handler or output-plugin initializer (ANKUS245, ANKUS246, ANKUS266, ANKUS267, ANKUS276, ANKUS277, ANKUS343, ANKUS524), SQL value metadata on a worker, initializer or trigger (ANKUS225, ANKUS248, ANKUS270, ANKUS271), Rows or SetMode without a set result (ANKUS053), operator options an operator cannot take (ANKUS070, ANKUS071) and Schema on a managed type provider (ANKUS384). It leaves the source unchanged if other code still needs what it would remove.

Task and asynchronous iterator diagnostics are described in execution constraints.

C# PostgreSQL
bool boolean
sbyte "char" (the internal signed byte type)
short, int, long smallint, integer, bigint
uint oid
PgRelation regclass, a checked relation reference
PgItemPointer tid (tuple location)
float, double real, double precision
string text
Rune varchar holding exactly one Unicode scalar value
byte[] bytea
PgTextView, PgByteaView text, bytea with checked native borrowing
Guid uuid
PgInet, IPAddress inet
PgCidr, IPNetwork cidr
PgPoint, PgLine, PgLineSegment point, line, lseg
PgBox, PgCircle box, circle
PgPath, PgPolygon path, polygon
PgRange<T> Built-in range selected by the bound type
PgJson, PgJsonb json, jsonb
decimal, PgNumeric numeric
DateOnly, PgDate date
TimeOnly, PgTime time
PgTimeTz timetz
DateTime, PgTimestamp timestamp
DateTimeOffset, PgTimestampTz timestamptz
TimeSpan, PgInterval interval
T[], PgArray<T> Array of the corresponding scalar SQL type
PgAnyElement, PgAnyArray anyelement, anyarray
PgArrayView anyarray, with borrowed native cells
PgArrayView<T> The scalar element’s array type, with checked lazy conversion
PgCString, PgCStringView cstring, as exact owned or borrowed bytes
PgHeapTuple record, or a named composite type
[PgEnum] C# enums Generated PostgreSQL enum types
[PgType] classes, structs and enums Generated custom base types
[PgDatumType] classes, structs and enums The converter’s declared existing SQL type, also usable as an array element
PgDatum with [PgSqlType] The named PostgreSQL type, as a raw value
PgInternal internal (backend callback state)
void result void

string and byte[] inputs are managed copies. They remain valid after PostgreSQL releases the original storage. PgTextView and PgByteaView expose checked native views; scalar inputs expire at callback exit. See text and binary values for encoding, ownership, copying and returning views.

See date and time values for precision, time zones, and full-range PostgreSQL values.

See network values for IPv4/IPv6 prefixes and checked System.Net conversions. IPAddress requires a full-width host prefix.

See geometric values for coordinates, owned vertex collections, and PostgreSQL input validation.

See ranges for empty values, bound inclusion, canonicalization, and checked .NET bound types.

See arrays for dimensions, lower bounds, nullable elements, and variadic functions. byte[] is scalar bytea; byte[][] is bytea[].

Use reusable datum mappings for an existing SQL representation. Function inputs need a reader; outputs need a writer. Array signatures use the same element converter and type provider.

Use nullable C# types to receive SQL NULL:

[PgFunction]
public static string GreetOrDefault(string? name) => $"Hello, {name ?? "world"}!";

If every parameter is required, the generated SQL function is STRICT: PostgreSQL returns NULL without calling the method when any argument is NULL.

For mixed signatures, a NULL required argument still returns NULL without entering the method. Nullable arguments reach your code. A null result becomes SQL NULL.

An unhandled managed exception becomes a PostgreSQL ERROR with SQLSTATE 38000 (external_routine_exception). The managed stack unwinds first, so finally blocks and using scopes run normally.

Use PgException when the error needs a particular SQLSTATE, detail, or hint.