Skip to content
This page View Markdown Open in ChatGPT Open in Claude

C

Code in C# is a class with public static async Task<object?> Main(G g). It is compiled once, in memory, by the compiler of the .NET SDK, and runs on the SDK image (mcr.microsoft.com/dotnet/sdk:10.0 unless the settings name another version), with only the packages you add from NuGet.

In short

  • Write a class with public static async Task<object?> Main(G g).
  • It is compiled once, and runs on mcr.microsoft.com/dotnet/sdk:10.0 unless the settings name another version.
  • Refuse a request with throw new AmErrors(message, 400).
Hello.cs
1
2
3
4
5
6
7
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        return new { hello = g.Req.Body.GetString("name", "world") };
    }
}
Runtime .NET 8 or later, a version of mcr.microsoft.com/dotnet/sdk
Main a static method Main(G g) of any class : Task<object?>, Task, object? or void
file file static class Api : the class is known in its file only, so every piece of code can have its own Api and git still builds them as one project. public static class Api runs as well
Answer what Main returns, written as JSON; null lets the API go on
Calls of g.Sys typed : what a call takes is a class with a property for each key (new GetAll("mysql_8", "inventory", "customers") { Limit = 10 }), what it answers has its type (List<JsonObject>, JsonObject?, long, UpdateManyResult, or your own class : GetAll<Customer>)
Values System.Text.Json.Nodes : JsonObject, JsonArray, JsonNode, read by their types too (row.GetString("name"), row.GetInt64("id"), row.To<Customer>())
Usings System, System.Collections.Generic, System.Linq, System.Threading.Tasks, System.Text.Json, System.Text.Json.Nodes and Am are there already
Packages NuGet : Humanizer.Core 2.14.1
Printing Console.WriteLine goes to the console of the account

The request and the answer

Read the request, shape the answer
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        var status = g.Req.Query.GetString("status", "open");
        var signed = g.Req.Headers["x-am-authorization"] is not null;
        g.Res.StatusCode = 201;
        g.Res.Headers["X-Request-Kind"] = "orders";
        return new { customer = g.Req.Params["id"], status, signed };
    }
}
  • An anonymous object, a record, a list or a JsonNode : anything System.Text.Json writes is an answer. A byte[] is bytes : see below. NaN and infinities are null, a DateTime in UTC is 2026-01-02T03:04:05.678Z.
  • What you change of g.Req in a pre hook is what the API sees.
  • A body which is a list ([{…}, {…}], several rows to save) is read with g.Req["body"] as JsonArray ; g.Req.Body is an empty JsonObject then.

Bytes as the answer

A file, a picture, a PDF : return its bytes (a byte[], a Memory<byte>), or put Json.Bytes(bytes) in g.Res.Output, and name their kind of content. The answer is those bytes as they are, whatever the kind of content, with the headers of the code. (A text under a kind of bytes, image/gif, is read as base64.)

A picture as the answer
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        var picture = Convert.FromBase64String("R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=");
        g.Res.ContentType = "image/gif";
        g.Res.Headers["Content-Disposition"] = "inline; filename=\"pixel.gif\"";
        return picture;
    }
}

Calling API Maker : g.Sys

Each method is a call of the server, with its types. What it takes is a class with a property for each of its keys, made with what it is about (new GetAll("mysql_8", "inventory", "customers") { Limit = 10 }) ; what it answers is typed too : rows are a List<JsonObject>, a row a JsonObject?, a count a long, what an API describes a class with a property for each key (changed.UpdatedRowsCount).

Database calls
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        var rows = await g.Sys.Db.GetAll(new GetAll("mysql_8", "inventory", "customers") { Limit = 10, Sort = "-customer_id" });
        var two = await g.Sys.Db.GetById(new GetById("mysql_8", "inventory", "customers", 2));
        var count = await g.Sys.Db.Count(new Count("mysql_8", "inventory", "customers") { Find = new { country = "IN" } });
        return new { first = rows.FirstOrDefault(), name = two.GetString("first_name"), inIndia = count };
    }
}
  • Rows as a class of yours (a record whose properties are the columns) : List<Customer> customers = await g.Sys.Db.GetAll<Customer>(new GetAll(…)), await g.Sys.Db.GetById<Customer>(…), or row.To<Customer>() on any row.
  • A key the class has no property for is set by its name : new GetAll("mysql_8", "inventory", "customers") { ["cursor"] = "abc" }.
  • What a save answers : saved.Row, saved.Rows, saved.IsList. The generated APIs are under g.Sys.Db.Gen, the whole answer of a call under Full : await g.Sys.Db.Full.GetAll(…) gives an ApiResponse<List<JsonObject>> (Success, StatusCode, Data, TotalCount, Errors).

Calls at the same time are tasks :

Calls at the same time
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        var orders = g.Sys.Db.Count(new Count("mongodb", "shop", "orders"));
        var products = g.Sys.Db.Count(new Count("mongodb", "shop", "products"));
        await Task.WhenAll(orders, products);
        return new { orders = orders.Result, products = products.Result };
    }
}

Rows as they arrive

Sum a large table
1
2
3
4
5
6
7
8
9
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        var total = 0.0;
        await g.Sys.Db.GetAllByStream(new GetAll("mysql_8", "inventory", "orders"), row => total += row.GetDouble("amount"));
        return new { amount = total };
    }
}

The function of the rows can be Action<JsonObject> or Func<JsonObject, Task>.

System APIs and cache

Encrypt, hash, events and cache keys
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        var sealedData = await g.Sys.System.Encrypt(new { card = "4111" });
        var digest = await g.Sys.System.Hash("my text");
        await g.Sys.System.EmitEvent("order_created", new { id = 7 }, new[] { "notify_shop" });
        await g.Sys.Cache.SetKey("last_order", "7", 600);
        return new { sealedData, digest, last = await g.Sys.Cache.GetKey("last_order") };
    }
}

Errors

Refuse a request
1
2
3
4
5
6
7
8
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        if (g.Req.Body["quantity"] is null) throw new AmErrors("'quantity' is missing.", 400);
        return new { ok = true };
    }
}
Catch the errors of a call
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        try
        {
            await g.Sys.Db.RemoveById(new RemoveById("mysql_8", "inventory", "customers", 404));
            return new { removed = true };
        }
        catch (AmErrors refused)
        {
            g.Logger.Warn("not removed :", refused.Message);
            return new { removed = false, code = refused.Code };
        }
    }
}

Any other exception answers 500 with its type and message, the message of what it happened because of (a type initializer which failed), and the lines of your code : at Api.Helper(Int32 x) in orders.cs:line 10.

Logs, printing and shared values

Logs and shared values
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        g.Logger.Log("order", 7, "paid");               // "order 7 paid"
        g.Logger.Error(new { step = "payment" });        // objects as indented JSON
        Console.WriteLine("kept in the console of the account");
        // Lives as long as the runner, shared by every request it serves.
        return g.Shared.AddOrUpdate("calls", 1, (_, old) => (int)old! + 1);
    }
}

Hooks

Pre hook : only my rows
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        var query = g.Req.Query;
        if (query["find"] is not JsonObject find) query["find"] = find = new JsonObject();
        find["owner_id"] = g.Req.Auth.GetObject("authAMDB")["id"]?.DeepClone();
        return null;
    }
}
Post hook : hide a column
1
2
3
4
5
6
7
8
9
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        if (g.Res.Output is JsonArray rows)
            foreach (var row in rows.OfType<JsonObject>()) row.Remove("password");
        return null;
    }
}

Utility classes

A utility class written in C# is compiled with the other C# code of the account, which names what it declares.

The utility class Tax
public static class Tax
{
    public const double Rate = 0.2;

    public static double Add(double amount) => Round.Cents(amount * (1 + Rate));   // Round : another utility class
}
A custom API which uses it
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        return new { total = Tax.Add(g.Req.Body.GetDouble("amount")) };
    }
}
  • Every class of the account is compiled with every piece of code which runs : internal and public types are both there, and what a class keeps in a static field is kept per piece of code.
  • A Main(G g) of a utility class is never taken for the Main of the code.
  • In the Git repository the class is src/utility_classes/<folder>---<name>/<name>.cs : ApiMaker.csproj compiles it with everything else.

Test cases

A test case written in C# has a method per test : every static method whose name starts with Test and which takes G g, in any class. What it throws is its failure.

A test case
public static class ShippingTests
{
    public static async Task TestTheCostOfAParcel(G g)
    {
        await g.Sys.Test.Mock(new Mock("db.getAll") { When = new { collection = "rates" }, Return = new[] { new { per_kg = 2.5 } } });
        var output = await g.Sys.Test.RunCustomApi(new RunCustomApi("shippingCost") { Body = new { weight = 4 } });
        if (output.GetDouble("cost") != 10) throw new Exception("the cost is " + output?["cost"]);
    }
}
  • g.Sys.Test : RunCustomApi(new RunCustomApi(…)), RunCustomApiFull(…) for the whole answer (Data, StatusCode, Headers, Logs), Mock(new Mock(…)), Mocks(…), ClearMocks(method), Calls(method).
  • The tests run in the order they are written. The custom API under test can be written in any language.

Packages

Add Humanizer.Core in Sandbox Settings, then :

A package of NuGet
1
2
3
4
5
6
using Humanizer;

file static class Api
{
    public static object? Main(G g) => new { words = 3.ToWords() };
}
  • Versions are NuGet versions : 13.0.3, floating 13.*, ranges [1.0,2.0).
  • The packages are restored with the runner when the image is built; the compiler sees them at once.

How it runs

  • The code is compiled once per version, in memory, with its symbols : stack traces say its lines. The first save of code starts that compilation in the background, so the first request does not wait for it.
  • Every request is a task of the runner; requests run at the same time. A thread of the pool which has nothing left to do waits at once, it does not spin first (DOTNET_ThreadPool_UnfairSemaphoreSpinLimit=0) : a request which arrives by itself then costs less than half the processor time. A variable of that name in the environment of your sandboxes is kept as you set it.
  • nullable is on : the compiler warns about null, it does not refuse.
  • A JsonNode belongs to one parent : to put a node of the request into another object, DeepClone() it.

In the code editor

The editor completes g as C# writes it, the names of your instances, databases and tables, and the columns of a table on the rows a call answers. More in the code editor in every language.

Where the editor helps
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        // g.  →  Req, Res, Sys, Logger, Shared      g.Sys.Db.  →  GetAll, GetById, Query, Gen, Full…
        g.Logger.Log(g.Req.Body.GetString("name"));  // g.Req.  →  Body, Query, Headers, Auth…      g.Res.  →  StatusCode, Headers…
        var rows = await g.Sys.Db.GetAll(new GetAll("mysql_8", "inventory", "customers") { Limit = 10 });
        //         new GetAll("  →  your instances, then its databases, then their tables      { L  →  Limit = , Find = , Sort = …
        foreach (var row in rows)
            g.Logger.Log(row.GetString("first_name"), row["last_name"]);  // row.GetString("  and  row["  →  the columns of customers
        return rows[0]["first_name"];
    }
}
  • What a call answers has its type : on the rows of a GetAll the columns are offered inside the quotes of GetString, GetInt64, GetDouble, GetObject… and of an index (rows[0][", row["), in a foreach, in a lambda (rows.Select(row => row.GetString("…"))) and in the function given to GetAllByStream. changed. (an UpdateMany) offers UpdatedRowsCount, saved. offers Row, Rows and IsList, the whole answer of g.Sys.Db.Full offers Success, Data, Errors…
  • Between the braces of what a call takes the editor offers its properties as names, each with what it is for : after new GetAll("mysql_8", "inventory", "customers") { it offers Limit, Find, Sort, Select… Inside Find = new { it offers the columns of the table, inside Select = " and Sort = " too.
  • g.Req. and g.Res. offer a property for each key of the request and of the answer ; g.Req.Auth. the opened tokens, written as C# reads a key (g.Req.Auth["authAMUser"], and ?["name"] after ?.).
  • The columns follow a row the same way : rows[0]. then first_name writes rows[0]["first_name"].

Reference

g. C#
Req Body, Query, Params, Headers, Auth, ReqInfo (each a JsonObject), EventData, IsApiRequestFromUser
Res StatusCode, ContentType, Headers, Shared, Output, Errors, Warnings
Sys.Db GetAll(GetAll), GetById(GetById), Query(Query), SaveSingleOrMultiple(Save), MasterSave(Save), ArrayOperations(ArrayOperations), UpdateById(UpdateById), UpdateMany(UpdateMany), ReplaceById(UpdateById), RemoveById(RemoveById), RemoveByQuery(RemoveByQuery), Aggregate(Aggregate), Count(Count), Distinct(Distinct), DistinctQuery(Distinct), GetAllByStream, QueryByStream ; GetAll<T>, GetById<T>… for rows as your own class ; Sys.Db.Gen.GetAllGen…
Sys.Db.Full, Sys.Db.Gen.Full, Sys.System.Full, Sys.Cache.Full the same calls, each with its whole answer : ApiResponse<T> (Success, StatusCode, Data, TotalCount, Errors, Warnings)
Sys.System Encrypt, EncryptWith, Decrypt, DecryptWith, Hash, GetSecret, GetSecrets, CallExternalApi, CallExternalApis, GetToken, GetTokens, ExecuteQuery, GetTableMeta, GetTableMetas, CreateIndexes, DropIndexes, DropIndexesOfMany, GetIndexes, GetIndexesOfMany, EmitEvent, EmitEventWS, IsValidDataForTable(s), IsValidDataForCustomAPI(s), MultiTenantInstance(s)Updated, IsValidConnectionString(s), GetLock, ReleaseLock
Sys.Cache GetKey, GetKeys, SetKey, SetKeys, RemoveKey, RemoveKeys, ResetCacheDB, ResetCacheDBs, ResetCacheCustomApis, ResetCacheCustomApisOfTenant, ResetCacheSystemApis, ResetCacheSystemApisOfTenant
Sys.Call("db.getAll", …) any API by its name in JavaScript, with its arguments as they are
a JsonObject, a JsonNode GetString, GetInt64, GetInt32, GetDouble, GetBoolean, GetObject, GetArray, GetObjects, GetStrings, To<T>()
Logger Log, Debug, Info, Warn, Error
Shared ConcurrentDictionary<string, object?> of the runner
errors throw new AmErrors(message, code) or new AmErrors(JsonArray); .Errors (a List<ResponseError>), .Code

Debugging

  • The debug runner of C# compiles your code with its symbols, under its file of the repository : netcoredbg attaches to it for the Debug tab of the API testing page.
  • Learn more Debugging.