appwrite-dotnet について
Appwrite .NET SDKは、Appwriteを利用するアプリを開発する.NET向けライブラリです。認証、ユーザー、テーブル、ストレージ、チーム、関数、権限を扱うAPIを提供します。
主な機能
- 01Appwriteクライアントの設定
- 02ユーザーとチームの管理
- 03テーブルと行の作成
- 04ファイルのアップロードと一覧表示
- 05サーバーレス関数の実行
- 06リソース権限の設定
活用シーン
- 01.NETアプリへのAppwriteユーザー管理の追加
- 02テーブル行の読み書き
- 03アクセス規則を指定したファイルのアップロード
- 04Appwrite関数の実行
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ appwrite/ skills --skill appwrite-dotnetインストール後、Claude Code で/appwrite-dotnetを入力して読み込まれたか確認します。
詳細
Appwrite .NET SDK
インストール
dotnet add package Appwrite
クライアントのセットアップ
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"));
コード例
ユーザー管理
var users = new Users(client);
// Create user
var user = await users.Create(ID.Unique(), "[email protected]", null, "password123", "User Name");
// List users
var list = await users.List(new List<string> { Query.Limit(25) });
// Get user
var fetched = await users.Get("[USER_ID]");
// Delete user
await users.Delete("[USER_ID]");
データベース操作
Note: すべての新しいコードでは、
TablesDBを使用してください(非推奨のDatabasesクラスではありません)。Databasesは、既存のコードベースがすでに依存している場合、またはユーザーが明示的に要求した場合にのみ使用してください。Tip: すべての SDK メソッド呼び出しでは、名前付き引数(例:
databaseId: "...")を優先してください。位置引数は、既存のコードベースがすでに使用している場合、またはユーザーが明示的に要求した場合にのみ使用してください。
var tablesDB = new TablesDB(client);
// Create database
var db = await tablesDB.Create(ID.Unique(), "My Database");
// Create row
var doc = await tablesDB.CreateRow("[DATABASE_ID]", "[TABLE_ID]", ID.Unique(),
new Dictionary<string, object> { { "title", "Hello World" } });
// Query rows
var results = await tablesDB.ListRows("[DATABASE_ID]", "[TABLE_ID]",
new List<string> { Query.Equal("title", "Hello World"), Query.Limit(10) });
// Get row
var row = await tablesDB.GetRow("[DATABASE_ID]", "[TABLE_ID]", "[ROW_ID]");
// Update row
await tablesDB.UpdateRow("[DATABASE_ID]", "[TABLE_ID]", "[ROW_ID]",
new Dictionary<string, object> { { "title", "Updated" } });
// Delete row
await tablesDB.DeleteRow("[DATABASE_ID]", "[TABLE_ID]", "[ROW_ID]");
文字列カラムの型
Note: レガシーの
string型は非推奨です。すべての新しいカラムには明示的なカラム型を使用してください。
| 型 | 最大文字数 | インデックス | ストレージ |
|---|---|---|---|
varchar | 16,383 | 完全インデックス(サイズが 768 以下の場合) | 行内にインライン保存 |
text | 16,383 | プレフィックスのみ | オフページ |
mediumtext | 4,194,303 | プレフィックスのみ | オフページ |
longtext | 1,073,741,823 | プレフィックスのみ | オフページ |
varcharはインラインで保存され、64 KB の行サイズ制限にカウントされます。名前、スラッグ、識別子などの短くインデックスされたフィールドに適しています。text、mediumtext、longtextはオフページで保存されるため(行内には 20 バイトのポインタのみが存在します)、行サイズの予算を消費しません。これらの型にはsizeは必要ありません。
// Create table with explicit string column types
await tablesDB.CreateTable("[DATABASE_ID]", ID.Unique(), "articles",
new List<object> {
new { key = "title", type = "varchar", size = 255, required = true }, // inline, fully indexable
new { key = "summary", type = "text", required = false }, // off-page, prefix index only
new { key = "body", type = "mediumtext", required = false }, // up to ~4 M chars
new { key = "raw_data", type = "longtext", required = false }, // up to ~1 B chars
});
クエリメソッド
// Filtering
Query.Equal("field", "value") // == (or pass array for IN)
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) // 1 <= field <= 100
Query.IsNull("field") // is null
Query.IsNotNull("field") // is not null
Query.StartsWith("field", "prefix") // starts with
Query.EndsWith("field", "suffix") // ends with
Query.Contains("field", "sub") // contains
Query.Search("field", "keywords") // full-text search (requires index)
// Sorting
Query.OrderAsc("field")
Query.OrderDesc("field")
// Pagination
Query.Limit(25) // max rows (default 25, max 100)
Query.Offset(0) // skip N rows
Query.CursorAfter("[ROW_ID]") // cursor pagination (preferred)
Query.CursorBefore("[ROW_ID]")
// Selection & Logic
Query.Select(new List<string> { "field1", "field2" })
Query.Or(new List<string> { Query.Equal("a", 1), Query.Equal("b", 2) }) // OR
Query.And(new List<string> { Query.GreaterThan("age", 18), Query.LessThan("age", 65) }) // AND (default)
Query.Relationship("comments", new List<string> { Query.Equal("approved", true), Query.Limit(25) }) // related documents
// Aggregations (optional alias)
Query.Count("*", "total")
Query.Sum("amount", "revenue")
Query.Avg("score", "avgScore")
Query.Min("price", "minPrice")
Query.Max("price", "maxPrice")
// Grouping
Query.GroupBy(new List<string> { "status", "year" })
Query.Having(new List<string> { Query.GreaterThan("total", 1) })
Query.Distinct()
// Joins (table id, left key, right key, operator, alias)
Query.Join("orders", "$id", "customerId")
Query.LeftJoin("orders", "$id", "customerId", "=", "ord")
Query.LeftJoin("orders", "ord", new List<string> {
Query.On("$id", "customerId"),
Query.Equal("ord.status", "paid"),
})
// Fluent builder. Pass Build() into list methods.
var queries = new Query.Builder()
.Equal("status", "published")
.LeftJoin("orders", "$id", "customerId", "=", "ord")
.Count("*", "total")
.GroupBy(new List<string> { "status" })
.Limit(25)
.Build();
await tablesDB.ListRows("[DATABASE_ID]", "[TABLE_ID]", queries);
ファイルストレージ
var storage = new Storage(client);
// Upload file
var file = await storage.CreateFile("[BUCKET_ID]", ID.Unique(), InputFile.FromPath("/path/to/file.png"));
// List files
var files = await storage.ListFiles("[BUCKET_ID]");
// Delete file
await storage.DeleteFile("[BUCKET_ID]", "[FILE_ID]");
InputFile ファクトリメソッド
using Appwrite.Models;
InputFile.FromPath("/path/to/file.png") // from filesystem path
InputFile.FromBytes(byteArray, "file.png", "image/png") // from byte[]
InputFile.FromStream(stream, "file.png", "image/png", size) // from Stream (size required)
Teams
var teams = new Teams(client);
// Create team
var team = await teams.Create(ID.Unique(), "Engineering");
// List teams
var list = await teams.List();
// Create membership (invite user by email)
var membership = await teams.CreateMembership(
teamId: "[TEAM_ID]",
roles: new List<string> { "editor" },
email: "[email protected]"
);
// List memberships
var members = await teams.ListMemberships("[TEAM_ID]");
// Update membership roles
await teams.UpdateMembership("[TEAM_ID]", "[MEMBERSHIP_ID]", new List<string> { "admin" });
// Delete team
await teams.Delete("[TEAM_ID]");
ロールベースのアクセス: 権限を設定する際は、すべてのチームメンバーには
Role.Team("[TEAM_ID]")を、特定のチームロールにはRole.Team("[TEAM_ID]", "editor")を使用してください。
サーバーレス関数
var functions = new Functions(client);
// Execute function
var execution = await functions.CreateExecution("[FUNCTION_ID]", body: "{\"key\": \"value\"}");
// List executions
var executions = await functions.ListExecutions("[FUNCTION_ID]");
関数ハンドラーの作成(.NET ランタイム)
// src/Main.cs — Appwrite Function entry point
using System.Text.Json;
public async Task<RuntimeOutput> Main(RuntimeContext context)
{
// context.Req.Body — raw body (string)
// context.Req.BodyJson — parsed JSON (JsonElement)
// context.Req.Headers — headers (Dictionary)
// context.Req.Method — HTTP method
// context.Req.Path — URL path
// context.Req.Query — query params (Dictionary)
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 }); // JSON
// context.Res.Text("Hello"); // plain text
// context.Res.Empty(); // 204
// context.Res.Redirect("https://..."); // 302
}
サーバーサイドレンダリング(SSR)認証
.NET フレームワーク(ASP.NET、Blazor Server など)を使用する SSR アプリは、認証を処理するために サーバー SDK を使用します。2 つのクライアントが必要です。
- 管理者クライアント — API キーを使用し、セッションを作成し、レート制限をバイパスします(再利用可能なシングルトン)
- セッションクライアント — セッション Cookie を使用し、ユーザーの代理として動作します(リクエストごとに作成し、共有しないでください)
using Appwrite;
using Appwrite.Services;
// Admin client (reusable)
var adminClient = new Client()
.SetEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.SetProject("[PROJECT_ID]")
.SetKey(Environment.GetEnvironmentVariable("APPWRITE_API_KEY"));
// Session client (create per-request)
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);
}
メール/パスワードログイン(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);
// Cookie name must be a_session_<PROJECT_ID>
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 });
});
認証済みリクエスト
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 フロー
// Step 1: Redirect to OAuth provider
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);
});
// Step 2: Handle callback — exchange token for session
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 のセキュリティ: XSS を防ぐために、常に
HttpOnly、Secure、SameSite = SameSiteMode.Strictを使用してください。Cookie 名はa_session_<PROJECT_ID>である必要があります。
ユーザーエージェントの転送: デバッグとセキュリティのためにエンドユーザーのブラウザ情報を記録するには、
sessionClient.SetForwardedUserAgent(ctx.Request.Headers["User-Agent"])を呼び出してください。
エラーハンドリング
using Appwrite;
try
{
var row = await tablesDB.GetRow("[DATABASE_ID]", "[TABLE_ID]", "[ROW_ID]");
}
catch (AppwriteException e)
{
Console.WriteLine(e.Message); // human-readable message
Console.WriteLine(e.Code); // HTTP status code (int)
Console.WriteLine(e.Type); // error type (e.g. "document_not_found")
Console.WriteLine(e.Response); // full response body
}
一般的なエラーコード:
| コード | 意味 |
|---|---|
401 | Unauthorized — セッション/API キーが欠落しているか無効です |
403 | Forbidden — 権限が不十分です |
404 | Not found — リソースが存在しません |
409 | Conflict — ID の重複または一意制約違反です |
429 | Rate limited — リクエストが多すぎます |
権限とロール(重要)
Appwrite は、リソースへのアクセスを制御するために権限文字列を使用します。各権限は、アクション(read、update、delete、create、または create + update + delete を許可する write)とロールターゲットを組み合わせたものです。デフォルトでは、行/ファイルレベルで権限が明示的に設定されているか、テーブル/バケットの設定から継承されていない限り、どのユーザーもアクセスできません。権限は、Permission および Role ヘルパーで構築された文字列の配列です。
using Appwrite;
// Permission and Role are included in the main namespace
権限を持つデータベース行
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]")), // specific user can read
Permission.Update(Role.User("[USER_ID]")), // specific user can update
Permission.Read(Role.Team("[TEAM_ID]")), // all team members can read
Permission.Read(Role.Any()), // anyone (including guests) can read
});
権限付きのファイルアップロード
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]")),
});
権限を設定するタイミング: リソースごとのアクセス制御が必要な場合は、行/ファイルレベルの権限を設定します。テーブル内のすべての行が同じルールを共有する場合は、テーブル/バケットレベルで権限を設定し、行の権限は空のままにします。
よくある間違い:
- 権限の設定忘れ — リソースがすべてのユーザー(作成者を含む)からアクセスできなくなります
write/update/deleteを指定したRole.Any()— 未認証のゲストを含む任意のユーザーがリソースを変更または削除できるようになります- 機密データに対する
Permission.Read(Role.Any())— リソースが誰でも読み取り可能になります