| name | appwrite-dotnet |
| description | Appwrite .NET SDK skill. Use when building server-side C# or .NET applications with Appwrite, including ASP.NET and Blazor integrations. Covers user management, database/table CRUD, file storage, and functions via API keys. |
Appwrite .NET SDK
Installation
dotnet add package Appwrite
Setting Up the Client
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.SetProject(Environment.GetEnvironmentVariable("APPWRITE_PROJECT_ID"))
.SetKey(Environment.GetEnvironmentVariable("APPWRITE_API_KEY"));
Code Examples
User Management
var users = new Users(client);
var user = await users.Create(ID.Unique(), "user@example.com", null, "password123", "User Name");
var list = await users.List(new List<string> { Query.Limit(25) });
var fetched = await users.Get("[USER_ID]");
await users.Delete("[USER_ID]");
Database Operations
Note: Use TablesDB (not the deprecated Databases class) for all new code. Only use Databases if the existing codebase already relies on it or the user explicitly requests it.
Tip: Prefer named arguments (e.g., databaseId: "...") for all SDK method calls. Only use positional arguments if the existing codebase already uses them or the user explicitly requests it.
var tablesDB = new TablesDB(client);
var db = await tablesDB.Create(ID.Unique(), "My Database");
var doc = await tablesDB.CreateRow("[DATABASE_ID]", "[TABLE_ID]", ID.Unique(),
new Dictionary<string, object> { { "title", "Hello World" } });
var results = await tablesDB.ListRows("[DATABASE_ID]", "[TABLE_ID]",
new List<string> { Query.Equal("title", "Hello World"), Query.Limit(10) });
var row = await tablesDB.GetRow("[DATABASE_ID]", "[TABLE_ID]", "[ROW_ID]");
await tablesDB.UpdateRow("[DATABASE_ID]", "[TABLE_ID]", "[ROW_ID]",
new Dictionary<string, object> { { "title", "Updated" } });
await tablesDB.DeleteRow("[DATABASE_ID]", "[TABLE_ID]", "[ROW_ID]");
String Column Types
Note: The legacy string type is deprecated. Use explicit column types for all new columns.
| Type | Max characters | Indexing | Storage |
|---|
varchar | 16,383 | Full index (if size ≤ 768) | Inline in row |
text | 16,383 | Prefix only | Off-page |
mediumtext | 4,194,303 | Prefix only | Off-page |
longtext | 1,073,741,823 | Prefix only | Off-page |
varchar is stored inline and counts towards the 64 KB row size limit. Prefer for short, indexed fields like names, slugs, or identifiers.
text, mediumtext, and longtext are stored off-page (only a 20-byte pointer lives in the row), so they don't consume the row size budget. size is not required for these types.
await tablesDB.CreateTable("[DATABASE_ID]", ID.Unique(), "articles",
new List<object> {
new { key = "title", type = "varchar", size = 255, required = true },
new { key = "summary", type = "text", required = false },
new { key = "body", type = "mediumtext", required = false },
new { key = "raw_data", type = "longtext", required = false },
});
Query Methods
Query.Equal("field", "value")
Query.NotEqual("field", "value")
Query.LessThan("field", 100)
Query.LessThanEqual("field", 100)
Query.GreaterThan("field", 100)
Query.GreaterThanEqual("field", 100)
Query.Between("field", 1, 100)
Query.IsNull("field")
Query.IsNotNull("field")
Query.StartsWith("field", "prefix")
Query.EndsWith("field", "suffix")
Query.Contains("field", "sub")
Query.Search("field", "keywords")
Query.OrderAsc("field")
Query.OrderDesc("field")
Query.Limit(25)
Query.Offset(0)
Query.CursorAfter("[ROW_ID]")
Query.CursorBefore("[ROW_ID]")
Query.Select(new List<string> { "field1", "field2" })
Query.Or(new List<string> { Query.Equal("a", 1), Query.Equal("b", 2) })
Query.And(new List<string> { Query.GreaterThan("age", 18), Query.LessThan("age", 65) })
File Storage
var storage = new Storage(client);
var file = await storage.CreateFile("[BUCKET_ID]", ID.Unique(), InputFile.FromPath("/path/to/file.png"));
var files = await storage.ListFiles("[BUCKET_ID]");
await storage.DeleteFile("[BUCKET_ID]", "[FILE_ID]");
InputFile Factory Methods
using Appwrite.Models;
InputFile.FromPath("/path/to/file.png")
InputFile.FromBytes(byteArray, "file.png", "image/png")
InputFile.FromStream(stream, "file.png", "image/png", size)
Teams
var teams = new Teams(client);
var team = await teams.Create(ID.Unique(), "Engineering");
var list = await teams.List();
var membership = await teams.CreateMembership(
teamId: "[TEAM_ID]",
roles: new List<string> { "editor" },
email: "user@example.com"
);
var members = await teams.ListMemberships("[TEAM_ID]");
await teams.UpdateMembership("[TEAM_ID]", "[MEMBERSHIP_ID]", new List<string> { "admin" });
await teams.Delete("[TEAM_ID]");
Role-based access: Use Role.Team("[TEAM_ID]") for all team members or Role.Team("[TEAM_ID]", "editor") for a specific team role when setting permissions.
Serverless Functions
var functions = new Functions(client);
var execution = await functions.CreateExecution("[FUNCTION_ID]", body: "{\"key\": \"value\"}");
var executions = await functions.ListExecutions("[FUNCTION_ID]");
Writing a Function Handler (.NET runtime)
using System.Text.Json;
public async Task<RuntimeOutput> Main(RuntimeContext context)
{
context.Log($"Processing: {context.Req.Method} {context.Req.Path}");
if (context.Req.Method == "GET")
return context.Res.Json(new { message = "Hello from Appwrite Function!" });
return context.Res.Json(new { success = true });
}
Server-Side Rendering (SSR) Authentication
SSR apps using .NET frameworks (ASP.NET, Blazor Server, etc.) use the server SDK to handle auth. You need two clients:
- Admin client — uses an API key, creates sessions, bypasses rate limits (reusable singleton)
- Session client — uses a session cookie, acts on behalf of a user (create per-request, never share)
using Appwrite;
using Appwrite.Services;
var adminClient = new Client()
.SetEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.SetProject("[PROJECT_ID]")
.SetKey(Environment.GetEnvironmentVariable("APPWRITE_API_KEY"));
var sessionClient = new Client()
.SetEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.SetProject("[PROJECT_ID]");
var session = Request.Cookies["a_session_[PROJECT_ID]"];
if (session != null)
{
sessionClient.SetSession(session);
}
Email/Password Login (ASP.NET Minimal API)
app.MapPost("/login", async (HttpContext ctx, LoginRequest body) =>
{
var account = new Account(adminClient);
var session = await account.CreateEmailPasswordSession(body.Email, body.Password);
ctx.Response.Cookies.Append("a_session_[PROJECT_ID]", session.Secret, new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict,
Path = "/",
});
return Results.Ok(new { success = true });
});
Authenticated Requests
app.MapGet("/user", async (HttpContext ctx) =>
{
var session = ctx.Request.Cookies["a_session_[PROJECT_ID]"];
if (session == null) return Results.Unauthorized();
var sessionClient = new Client()
.SetEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.SetProject("[PROJECT_ID]")
.SetSession(session);
var account = new Account(sessionClient);
var user = await account.Get();
return Results.Ok(user);
});
OAuth2 SSR Flow
app.MapGet("/oauth", async () =>
{
var account = new Account(adminClient);
var redirectUrl = await account.CreateOAuth2Token(
provider: OAuthProvider.Github,
success: "https://example.com/oauth/success",
failure: "https://example.com/oauth/failure"
);
return Results.Redirect(redirectUrl);
});
app.MapGet("/oauth/success", async (HttpContext ctx, string userId, string secret) =>
{
var account = new Account(adminClient);
var session = await account.CreateSession(userId, secret);
ctx.Response.Cookies.Append("a_session_[PROJECT_ID]", session.Secret, new CookieOptions
{
HttpOnly = true, Secure = true, SameSite = SameSiteMode.Strict, Path = "/",
});
return Results.Ok(new { success = true });
});
Cookie security: Always use HttpOnly, Secure, and SameSite = SameSiteMode.Strict to prevent XSS. The cookie name must be a_session_<PROJECT_ID>.
Forwarding user agent: Call sessionClient.SetForwardedUserAgent(ctx.Request.Headers["User-Agent"]) to record the end-user's browser info for debugging and security.
Error Handling
using Appwrite;
try
{
var row = await tablesDB.GetRow("[DATABASE_ID]", "[TABLE_ID]", "[ROW_ID]");
}
catch (AppwriteException e)
{
Console.WriteLine(e.Message);
Console.WriteLine(e.Code);
Console.WriteLine(e.Type);
Console.WriteLine(e.Response);
}
Common error codes:
| Code | Meaning |
|---|
401 | Unauthorized — missing or invalid session/API key |
403 | Forbidden — insufficient permissions |
404 | Not found — resource does not exist |
409 | Conflict — duplicate ID or unique constraint |
429 | Rate limited — too many requests |
Permissions & Roles (Critical)
Appwrite uses permission strings to control access to resources. Each permission pairs an action (read, update, delete, create, or write which grants create + update + delete) with a role target. By default, no user has access unless permissions are explicitly set at the row/file level or inherited from the table/bucket settings. Permissions are arrays of strings built with the Permission and Role helpers.
using Appwrite;
Database Row with Permissions
var doc = await tablesDB.CreateRow("[DATABASE_ID]", "[TABLE_ID]", ID.Unique(),
new Dictionary<string, object> { { "title", "Hello World" } },
new List<string>
{
Permission.Read(Role.User("[USER_ID]")),
Permission.Update(Role.User("[USER_ID]")),
Permission.Read(Role.Team("[TEAM_ID]")),
Permission.Read(Role.Any()),
});
File Upload with Permissions
var file = await storage.CreateFile("[BUCKET_ID]", ID.Unique(),
InputFile.FromPath("/path/to/file.png"),
new List<string>
{
Permission.Read(Role.Any()),
Permission.Update(Role.User("[USER_ID]")),
Permission.Delete(Role.User("[USER_ID]")),
});
When to set permissions: Set row/file-level permissions when you need per-resource access control. If all rows in a table share the same rules, configure permissions at the table/bucket level and leave row permissions empty.
Common mistakes:
- Forgetting permissions — the resource becomes inaccessible to all users (including the creator)
Role.Any() with write/update/delete — allows any user, including unauthenticated guests, to modify or remove the resource
Permission.Read(Role.Any()) on sensitive data — makes the resource publicly readable