AgentDBDocs

ASP.NET Core

Build an ASP.NET Core minimal API in front of AgentDB. A named HttpClient holds the key. A static page in wwwroot/ calls the API on the same origin.

This page uses the aspnet-minimal starter. The smoke test builds it with dotnet build and runs it (npm run docs:starters). It needs no NuGet packages.

#Where the key lives

In configuration: environment variables in production, user secrets during development. Never in wwwroot/ and never in appsettings.json in source control.

Shell
dotnet user-secrets init
dotnet user-secrets set AGENTDB_WORKSPACE ws_your_workspace_id
dotnet user-secrets set AGENTDB_TOKEN replace-with-your-agent-key

#The AgentDB client

C#examples/starters/aspnet-minimal/Program.cs
var apiUrl = (builder.Configuration["AGENTDB_API_URL"] ?? "https://app.agentdb.link").TrimEnd('/');
var workspace = builder.Configuration["AGENTDB_WORKSPACE"] ?? throw new InvalidOperationException("Set AGENTDB_WORKSPACE.");
var token = builder.Configuration["AGENTDB_TOKEN"] ?? throw new InvalidOperationException("Set AGENTDB_TOKEN (an agent key, role editor).");

builder.Services.AddHttpClient("agentdb", http =>
{
    http.BaseAddress = new Uri($"{apiUrl}/v1/workspaces/{workspace}/");
    http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
    http.Timeout = TimeSpan.FromSeconds(15);
});

Every call goes through one helper. It returns the data part, or the status and error:

C#examples/starters/aspnet-minimal/Program.cs
public static async Task<(int Status, JsonNode? Data, JsonNode? Error)> Call(IHttpClientFactory factory, HttpMethod method, string path, JsonNode? body = null, string? idempotencyKey = null)
{
    using var req = new HttpRequestMessage(method, path);
    if (body is not null) req.Content = JsonContent.Create(body);
    if (idempotencyKey is not null) req.Headers.Add("Idempotency-Key", idempotencyKey);
    try
    {
        using var res = await factory.CreateClient("agentdb").SendAsync(req);
        var json = JsonNode.Parse(await res.Content.ReadAsStringAsync());
        return res.IsSuccessStatusCode ? ((int)res.StatusCode, json?["data"], null) : ((int)res.StatusCode, null, json?["error"]);
    }
    catch (Exception e) when (e is HttpRequestException or TaskCanceledException or JsonException)
    {
        return (0, null, null);
    }
}

public static IResult ErrorResult(int status, JsonNode? error)
{
    var code = error?["code"]?.GetValue<string>();
    if (code == "VERSION_CONFLICT") return Results.Json(new { error = "Someone changed it first. Reload and try again." }, statusCode: 409);
    if (status is 400 or 422) return Results.Json(new { error = error?["message"]?.GetValue<string>() ?? "Invalid input." }, statusCode: 400);
    Console.Error.WriteLine($"AgentDB call failed: {status} {code}");
    return Results.Json(new { error = "The data service is not available. Try again in a moment." }, statusCode: 502);
}

#The backend routes

C#examples/starters/aspnet-minimal/Program.cs
app.MapGet("/api/contacts", async (string? q, IHttpClientFactory factory) =>
{
    q = (q ?? "").Trim();
    if (q.Length > 100) q = q[..100];
    var where = new JsonArray();
    if (q != "") where.Add(new JsonObject { ["field"] = "name", ["op"] = "contains", ["value"] = q });
    var query = new JsonObject
    {
        ["object"] = "contact",
        ["where"] = new JsonObject { ["all"] = where },
        ["sort"] = new JsonArray(new JsonObject { ["field"] = "name", ["order"] = "asc" }),
        ["limit"] = 50,
    };
    var (status, data, error) = await AgentDb.Call(factory, HttpMethod.Post, "query", query);
    if (data is null) return AgentDb.ErrorResult(status, error);
    var contacts = data["records"]!.AsArray().Select(r => AgentDb.ContactOut(r!));
    return Results.Ok(new { contacts });
});

app.MapPost("/api/contacts", async (HttpRequest request, IHttpClientFactory factory) =>
{
    if (!request.HasJsonContentType()) return Results.Json(new { error = "Send JSON." }, statusCode: 415);
    var body = await request.ReadFromJsonAsync<JsonObject>();
    var name = (body?["name"]?.GetValue<string>() ?? "").Trim();
    var email = (body?["email"]?.GetValue<string>() ?? "").Trim();
    if (name.Length > 200) name = name[..200];
    if (email.Length > 254) email = email[..254];
    if (name == "") return Results.Json(new { error = "A contact needs a name." }, statusCode: 400);
    var data = new JsonObject { ["name"] = name };
    if (email != "") data["email"] = email;
    var submit = new string((request.Headers["X-Submit-Id"].FirstOrDefault() ?? Guid.NewGuid().ToString()).Where(c => char.IsAsciiLetterOrDigit(c) || c == '-').Take(64).ToArray());
    var (status, rec, error) = await AgentDb.Call(factory, HttpMethod.Post, "records", new JsonObject { ["object"] = "contact", ["data"] = data }, $"aspnet.{submit}");
    if (rec is null) return AgentDb.ErrorResult(status, error);
    return Results.Json(new { contact = AgentDb.ContactOut(rec) }, statusCode: 201);
});

#The frontend

wwwroot/index.html and wwwroot/app.js are the page from Plain HTML and JavaScript. UseDefaultFiles() and UseStaticFiles() serve them. With Blazor WebAssembly, the same rule applies: the WebAssembly app runs in the browser, so it calls this API and never AgentDB.

#Loading, empty and error states

ErrorResult maps AgentDB answers to short messages: 400 for validation, 409 for VERSION_CONFLICT, 502 for the rest. The page shows "Loading…", "No contacts match your search" or the error message.

#Live updates

Run a BackgroundService that follows the change feed (GET changes?after=<seq>&wait=1) and pushes to clients with server-sent events or SignalR. See the live dashboard starter for the loop and Live data for the options.

#Run it

Shell
cd examples/starters/aspnet-minimal
dotnet run