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.
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
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:
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
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
cd examples/starters/aspnet-minimal
dotnet run