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.0unless the settings name another version. - Refuse a request with
throw new AmErrors(message, 400).
| Hello.cs | |
|---|---|
| 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¶
- An anonymous object, a record, a list or a
JsonNode: anythingSystem.Text.Jsonwrites is an answer. Abyte[]is bytes : see below.NaNand infinities arenull, aDateTimein UTC is2026-01-02T03:04:05.678Z. - What you change of
g.Reqin a pre hook is what the API sees. - A body which is a list (
[{…}, {…}], several rows to save) is read withg.Req["body"] as JsonArray;g.Req.Bodyis an emptyJsonObjectthen.
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 | |
|---|---|
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).
- 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>(…), orrow.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 underg.Sys.Db.Gen, the whole answer of a call underFull:await g.Sys.Db.Full.GetAll(…)gives anApiResponse<List<JsonObject>>(Success,StatusCode,Data,TotalCount,Errors).
Calls at the same time are tasks :
Rows as they arrive¶
| Sum a large table | |
|---|---|
The function of the rows can be Action<JsonObject> or Func<JsonObject, Task>.
System APIs and cache¶
Errors¶
| Refuse a request | |
|---|---|
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¶
Hooks¶
| Pre hook : only my rows | |
|---|---|
| Post hook : hide a column | |
|---|---|
Utility classes¶
A utility class written in C# is compiled with the other C# code of the account, which names what it declares.
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
}
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 :
internalandpublictypes 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 theMainof the code. - In the Git repository the class is
src/utility_classes/<folder>---<name>/<name>.cs:ApiMaker.csprojcompiles 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.
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 | |
|---|---|
- Versions are NuGet versions :
13.0.3, floating13.*, 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. nullableis on : the compiler warns aboutnull, it does not refuse.- A
JsonNodebelongs 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.
- What a call answers has its type : on the rows of a
GetAllthe columns are offered inside the quotes ofGetString,GetInt64,GetDouble,GetObject… and of an index (rows[0][",row["), in aforeach, in a lambda (rows.Select(row => row.GetString("…"))) and in the function given toGetAllByStream.changed.(anUpdateMany) offersUpdatedRowsCount,saved.offersRow,RowsandIsList, the whole answer ofg.Sys.Db.FulloffersSuccess,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 offersLimit,Find,Sort,Select… InsideFind = new {it offers the columns of the table, insideSelect = "andSort = "too. g.Req.andg.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].thenfirst_namewritesrows[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.