mcpField
A
appwrite-dotnet

appwrite-dotnet

github.com/appwrite開発28547 回インストール1 週間前に更新
リポジトリ

appwrite-dotnet について

Appwrite .NET SDKは、Appwriteを利用するアプリを開発する.NET向けライブラリです。認証、ユーザー、テーブル、ストレージ、チーム、関数、権限を扱うAPIを提供します。

主な機能

  1. 01Appwriteクライアントの設定
  2. 02ユーザーとチームの管理
  3. 03テーブルと行の作成
  4. 04ファイルのアップロードと一覧表示
  5. 05サーバーレス関数の実行
  6. 06リソース権限の設定

活用シーン

  1. 01.NETアプリへのAppwriteユーザー管理の追加
  2. 02テーブル行の読み書き
  3. 03アクセス規則を指定したファイルのアップロード
  4. 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 型は非推奨です。すべての新しいカラムには明示的なカラム型を使用してください。

型最大文字数インデックスストレージ
varchar16,383完全インデックス(サイズが 768 以下の場合)行内にインライン保存
text16,383プレフィックスのみオフページ
mediumtext4,194,303プレフィックスのみオフページ
longtext1,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
}

一般的なエラーコード:

コード意味
401Unauthorized — セッション/API キーが欠落しているか無効です
403Forbidden — 権限が不十分です
404Not found — リソースが存在しません
409Conflict — ID の重複または一意制約違反です
429Rate 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()) — リソースが誰でも読み取り可能になります

コメント