次の方法で共有


.NET 用 Azure Mobile Apps クライアント ライブラリを使用する方法

手記

この製品は提供終了です。 .NET 8 以降を使用するプロジェクトの代わりに、Community Toolkit Datasync ライブラリを参照してください。

このガイドでは、Azure Mobile Apps 用の .NET クライアント ライブラリを使用して一般的なシナリオを実行する方法について説明します。 MAUI、Xamarin、Windows (WPF、UWP、WinUI) など、任意の .NET 6 または .NET Standard 2.0 アプリケーションで .NET クライアント ライブラリを使用します。

Azure Mobile Apps を初めて使用する場合は、最初に次のいずれかのクイック スタート チュートリアルを完了することを検討してください。

手記

この記事では、Microsoft Datasync Framework の最新 (v6.0) エディションについて説明します。 以前のクライアントについては、v4.2.0 のドキュメントを参照してください。

サポートされているプラットフォーム

.NET クライアント ライブラリは、次のような任意の .NET Standard 2.0 または .NET 6 プラットフォームをサポートします。

  • Android、iOS、および Windows プラットフォーム用の .NET MAUI。
  • Android API レベル 21 以降 (Xamarin および Android for .NET)。
  • iOS バージョン 12.0 以降 (Xamarin および .NET 用 iOS)。
  • ユニバーサル Windows プラットフォームビルド 19041 以降。
  • Windows Presentation Framework (WPF)。
  • Windows App SDK (WinUI 3)。
  • Xamarin.Forms

さらに、アヴァロニアウノプラットフォーム用のサンプルが作成されています。 TodoApp サンプル には、テストされた各プラットフォームの例が含まれています。

セットアップと前提条件

NuGet から次のライブラリを追加します。

  • Microsoft.Datasync.Client
  • オフライン テーブルを使用している場合は、Microsoft.Datasync.Client.SQLiteStore を します。

プラットフォーム プロジェクト (.NET MAUI など) を使用する場合は、プラットフォーム プロジェクトと共有プロジェクトにライブラリを追加してください。

サービス クライアントを作成する

次のコードでは、バックエンド テーブルとオフライン テーブルへのすべての通信を調整するために使用されるサービス クライアントを作成します。

var options = new DatasyncClientOptions 
{
    // Options set here
};
var client = new DatasyncClient("MOBILE_APP_URL", options);

前のコードでは、MOBILE_APP_URLASP.NET Core バックエンドの URL に置き換えます。 クライアントはシングルトンとして作成する必要があります。 認証プロバイダーを使用する場合は、次のように構成できます。

var options = new DatasyncClientOptions 
{
    // Options set here
};
var client = new DatasyncClient("MOBILE_APP_URL", authProvider, options);

認証プロバイダーの詳細については、このドキュメントの後半で説明します。

オプション

次のように、完全な (既定の) オプション セットを作成できます。

var options = new DatasyncClientOptions
{
    HttpPipeline = new HttpMessageHandler[](),
    IdGenerator = (table) => Guid.NewGuid().ToString("N"),
    InstallationId = null,
    OfflineStore = null,
    ParallelOperations = 1,
    SerializerSettings = null,
    TableEndpointResolver = (table) => $"/tables/{tableName.ToLowerInvariant()}",
    UserAgent = $"Datasync/5.0 (/* Device information */)"
};

HttpPipeline

通常、HTTP 要求は、要求を送信する前に、認証プロバイダー (現在認証されているユーザーの Authorization ヘッダーを追加) を介して要求を渡すことによって行われます。 必要に応じて、委任ハンドラーをさらに追加できます。 各要求は、サービスに送信される前に委任ハンドラーを通過します。 委任ハンドラーを使用すると、追加のヘッダーの追加、再試行、ログ記録機能の提供を行うことができます。

ハンドラーの委任の例は、ログ記録 と、この記事の後半で 要求ヘッダーを追加 するために提供されます。

IdGenerator

エンティティがオフライン テーブルに追加される場合は、ID が必要です。 ID が指定されていない場合は、ID が生成されます。 IdGenerator オプションを使用すると、生成される ID を調整できます。 既定では、グローバルに一意の ID が生成されます。 たとえば、次の設定では、テーブル名と GUID を含む文字列が生成されます。

var options = new DatasyncClientOptions 
{
    IdGenerator = (table) => $"{table}-{Guid.NewGuid().ToString("D").ToUpperInvariant()}"
}

InstallationId

InstallationId が設定されている場合、カスタム ヘッダー X-ZUMO-INSTALLATION-ID が各要求と共に送信され、特定のデバイス上のアプリケーションの組み合わせを識別します。 このヘッダーはログに記録でき、アプリの個別のインストールの数を決定できます。 InstallationIdを使用する場合は、一意のインストールを追跡できるように、ID をデバイス上の永続ストレージに格納する必要があります。

OfflineStore

OfflineStore は、オフライン データ アクセスを構成するときに使用されます。 詳細については、「オフライン テーブルを操作する」を参照してください。

ParallelOperations

オフライン同期プロセスの一部には、キューに登録された操作をリモート サーバーにプッシュすることが含まれます。 プッシュ操作がトリガーされると、操作は受信した順序で送信されます。 必要に応じて、最大 8 つのスレッドを使用してこれらの操作をプッシュできます。 並列操作では、クライアントとサーバーの両方でより多くのリソースを使用して、より高速に操作を完了します。 複数のスレッドを使用する場合、操作がサーバーに到着する順序は保証されません。

SerializerSettings

データ同期サーバーのシリアライザー設定を変更した場合は、クライアントの SerializerSettings に同じ変更を加える必要があります。 このオプションを使用すると、独自のシリアライザー設定を指定できます。

TableEndpointResolver

規則により、テーブルはリモート サービスの /tables/{tableName} パスに配置されます (サーバー コードの Route 属性で指定)。 ただし、任意のエンドポイント パスにテーブルを存在させることができます。 TableEndpointResolver は、テーブル名をリモート サービスと通信するためのパスに変換する関数です。

たとえば、次の例では、すべてのテーブルが /apiの下に配置されるように想定を変更します。

var options = new DatasyncClientOptions
{
    TableEndpointResolver = (table) => $"/api/{table}"
};

UserAgent

データ同期クライアントは、ライブラリのバージョンに基づいて適切な User-Agent ヘッダー値を生成します。 一部の開発者は、ユーザー エージェント ヘッダーがクライアントに関する情報を漏えいすると感じています。 UserAgent プロパティは、任意の有効なヘッダー値に設定できます。

リモート テーブルの操作

次のセクションでは、レコードを検索および取得し、リモート テーブル内のデータを変更する方法について説明します。 次のトピックについて説明します。

リモート テーブル参照を作成する

リモート テーブル参照を作成するには、GetRemoteTable<T>を使用します。

IRemoteTable<TodoItem> remoteTable = client.GetRemoteTable();

読み取り専用テーブルを返す場合は、IReadOnlyRemoteTable<T> バージョンを使用します。

IReadOnlyRemoteTable<TodoItem> remoteTable = client.GetRemoteTable();

モデル型は、サービスから ITableData コントラクトを実装する必要があります。 DatasyncClientData を使用して、必須フィールドを指定します。

public class TodoItem : DatasyncClientData
{
    public string Title { get; set; }
    public bool IsComplete { get; set; }
}

DatasyncClientData オブジェクトには次のものが含まれます。

  • Id (文字列) - アイテムのグローバルに一意の ID。
  • UpdatedAt (System.DataTimeOffset) - アイテムが最後に更新された日付/時刻。
  • Version (文字列) - バージョン管理に使用される不透明な文字列。
  • Deleted (ブール値) - true場合、項目は削除されます。

サービスでは、これらのフィールドが保持されます。 これらのフィールドは、クライアント アプリケーションの一部として調整しないでください。

モデルには、Newtonsoft.JSON 属性を使用して注釈を付けることができます。 テーブルの名前は、DataTable 属性を使用して指定できます。

[DataTable("todoitem")]
public class MyTodoItemClass : DatasyncClientData
{
    public string Title { get; set; }
    public bool IsComplete { get; set; }
}

または、GetRemoteTable() 呼び出しでテーブルの名前を指定します。

IRemoteTable<TodoItem> remoteTable = client.GetRemoteTable("todoitem");

クライアントは、URI として /tables/{tablename} パスを使用します。 テーブル名は、SQLite データベース内のオフライン テーブルの名前でもあります。

サポートされている型

プリミティブ型 (int、float、string など) とは別に、モデルでは次の型がサポートされています。

  • System.DateTime - MS 精度の ISO-8601 UTC 日付/時刻文字列として。
  • System.DateTimeOffset - MS 精度の ISO-8601 UTC 日付/時刻文字列として。
  • System.Guid - ハイフンで区切られた 32 桁の数字で書式設定されます。

リモート サーバーからデータを照会する

リモート テーブルは、次のような LINQ のようなステートメントで使用できます。

  • .Where() 句を使用したフィルター処理。
  • さまざまな .OrderBy() 句を使用した並べ替え。
  • .Select()を使用してプロパティを選択します。
  • .Skip().Take()を使用したページング。

クエリからアイテムをカウントする

クエリから返される項目の数が必要な場合は、テーブルまたはクエリで .LongCountAsync().CountItemsAsync() を使用できます。

// Count items in a table.
long count = await remoteTable.CountItemsAsync();

// Count items in a query.
long count = await remoteTable.Where(m => m.Rating == "R").LongCountAsync();

この方法では、サーバーへのラウンド トリップが発生します。 また、リストの設定中にカウントを取得することもできます (たとえば、余分なラウンドトリップを回避します)。

var enumerable = remoteTable.ToAsyncEnumerable() as AsyncPageable<T>;
var list = new List<T>();
long count = 0;
await foreach (var item in enumerable)
{
    count = enumerable.Count;
    list.Add(item);
}

この数は、テーブルの内容を取得する最初の要求の後に設定されます。

すべてのデータを返す

データは、IAsyncEnumerableを介して返されます。

var enumerable = remoteTable.ToAsyncEnumerable();
await foreach (var item in enumerable) 
{
    // Process each item
}

次のいずれかの終端句を使用して、IAsyncEnumerable<T> を別のコレクションに変換します。

T[] items = await remoteTable.ToArrayAsync();

Dictionary<string, T> items = await remoteTable.ToDictionaryAsync(t => t.Id);

HashSet<T> items = await remoteTable.ToHashSetAsync();

List<T> items = await remoteTable.ToListAsync();

バックグラウンドでは、リモート テーブルによって結果のページングが自動的に処理されます。 クエリを実行するために必要なサーバー側要求の数に関係なく、すべての項目が返されます。 これらの要素は、クエリ結果 (たとえば、remoteTable.Where(m => m.Rating == "R")) でも使用できます。

データ同期フレームワークには、スレッド セーフな監視可能なコレクションである ConcurrentObservableCollection<T> も用意されています。 このクラスは、通常、ObservableCollection<T> を使用してリスト (Xamarin Forms や MAUI リストなど) を管理する UI アプリケーションのコンテキストで使用できます。 テーブルまたはクエリから直接 ConcurrentObservableCollection<T> をクリアして読み込むことができます。

var collection = new ConcurrentObservableCollection<T>();
await remoteTable.ToObservableCollection(collection);

.ToObservableCollection(collection) を使用すると、個々の項目ではなくコレクション全体に対して CollectionChanged イベントが 1 回トリガーされ、再描画時間が短縮されます。

ConcurrentObservableCollection<T> には、述語駆動型の変更もあります。

// Add an item only if the identified item is missing.
bool modified = collection.AddIfMissing(t => t.Id == item.Id, item);

// Delete one or more item(s) based on a predicate
bool modified = collection.DeleteIf(t => t.Id == item.Id);

// Replace one or more item(s) based on a predicate
bool modified = collection.ReplaceIf(t => t.Id == item.Id, item);

述語駆動型の変更は、項目のインデックスが事前に不明な場合に、イベント ハンドラーで使用できます。

データのフィルター処理

.Where() 句を使用してデータをフィルター処理できます。 例えば:

var items = await remoteTable.Where(x => !x.IsComplete).ToListAsync();

フィルター処理は、IAsyncEnumerable より前のサービスと、IAsyncEnumerable の後のクライアントで行われます。 例えば:

var items = (await remoteTable.Where(x => !x.IsComplete).ToListAsync()).Where(x => x.Title.StartsWith("The"));

最初の .Where() 句 (不完全な項目のみを返す) はサービスで実行されますが、2 番目の .Where() 句 ("The"で始まる) はクライアントで実行されます。

Where 句は、OData サブセットに変換される操作をサポートします。 操作には次のものが含まれます。

  • 関係演算子 (==, !=, <, <=, >, >=),
  • 算術演算子 (+, -, /, *, %),
  • 数値の有効桁数 (Math.Floor, Math.Ceiling),
  • 文字列関数 (LengthSubstringReplaceIndexOfEqualsStartsWithEndsWith) (序数カルチャとインバリアント カルチャのみ)
  • Date プロパティ (Year, Month, Day, Hour, Minute, Second),
  • オブジェクトのプロパティにアクセスし、
  • これらの操作のいずれかを組み合わせた式。

データの並べ替え

データを並べ替えるには、プロパティ アクセサーで .OrderBy().OrderByDescending().ThenBy()、および .ThenByDescending() を使用します。

var items = await remoteTable.OrderBy(x => x.IsComplete).ThenBy(x => x.Title).ToListAsync();

並べ替えはサービスによって行われます。 並べ替え句で式を指定することはできません。 式で並べ替える場合は、クライアント側の並べ替えを使用します。

var items = await remoteTable.ToListAsync().OrderBy(x => x.Title.ToLowerCase());

プロパティの選択

サービスからデータのサブセットを返すことができます。

var items = await remoteTable.Select(x => new { x.Id, x.Title, x.IsComplete }).ToListAsync();

データのページを返す

ページングを実装するために、.Skip().Take() を使用して、データ セットのサブセットを返すことができます。

var pageOfItems = await remoteTable.Skip(100).Take(10).ToListAsync();

実際のアプリでは、前の例のようなクエリをポケットベル コントロールまたは同等の UI と共に使用して、ページ間を移動できます。

これまでに説明したすべての関数は加法であるため、それらを連結し続けることができます。 各チェーン呼び出しは、より多くのクエリに影響します。 もう 1 つの例:

var query = todoTable
                .Where(todoItem => todoItem.Complete == false)
                .Select(todoItem => todoItem.Text)
                .Skip(3).
                .Take(3);
List<string> items = await query.ToListAsync();

ID でリモート データを検索する

GetItemAsync 関数を使用して、特定の ID を持つデータベースからオブジェクトを検索できます。

TodoItem item = await remoteTable.GetItemAsync("37BBF396-11F0-4B39-85C8-B319C729AF6D");

取得しようとしている項目が論理的に削除された場合は、includeDeleted パラメーターを使用する必要があります。

// The following code will throw a DatasyncClientException if the item is soft-deleted.
TodoItem item = await remoteTable.GetItemAsync("37BBF396-11F0-4B39-85C8-B319C729AF6D");

// This code will retrieve the item even if soft-deleted.
TodoItem item = await remoteTable.GetItemAsync("37BBF396-11F0-4B39-85C8-B319C729AF6D", includeDeleted: true);

リモート サーバーにデータを挿入する

すべてのクライアント型には、IDという名前のメンバーが含まれている必要があります。既定では文字列です。 CRUD 操作とオフライン同期を実行するには、この ID が必要です。次のコードは、InsertItemAsync メソッドを使用して新しい行をテーブルに挿入する方法を示しています。 パラメーターには、.NET オブジェクトとして挿入するデータが含まれています。

var item = new TodoItem { Title = "Text", IsComplete = false };
await remoteTable.InsertItemAsync(item);
// Note that item.Id will now be set

挿入中に一意のカスタム ID 値が item に含まれていない場合、サーバーは ID を生成します。 生成された ID は、呼び出しが戻った後にオブジェクトを調べることで取得できます。

リモート サーバー上のデータを更新する

次のコードは、ReplaceItemAsync メソッドを使用して、同じ ID の既存のレコードを新しい情報で更新する方法を示しています。

// In this example, we assume the item has been created from the InsertItemAsync sample

item.IsComplete = true;
await remoteTable.ReplaceItemAsync(todoItem);

リモート サーバー上のデータを削除する

次のコードは、DeleteItemAsync メソッドを使用して既存のインスタンスを削除する方法を示しています。

// In this example, we assume the item has been created from the InsertItemAsync sample

await todoTable.DeleteItemAsync(item);

競合の解決とオプティミスティック コンカレンシー

2 つ以上のクライアントが同じ項目に同時に変更を書き込むことができます。 競合の検出がないと、前回の書き込みによって以前の更新が上書きされます。 オプティミスティック コンカレンシー制御 は、各トランザクションがコミットできるため、リソース ロックを使用しないことを前提としています。 オプティミスティック コンカレンシー制御では、データをコミットする前に、他のトランザクションによってデータが変更されていないことが確認されます。 データが変更された場合、トランザクションはロールバックされます。

Azure Mobile Apps では、モバイル アプリ バックエンドの各テーブルに対して定義されている version システム プロパティ列を使用して各項目の変更を追跡することで、オプティミスティック コンカレンシー制御をサポートしています。 レコードが更新されるたびに、Mobile Apps はそのレコードの version プロパティを新しい値に設定します。 更新要求のたびに、要求に含まれるレコードの version プロパティが、サーバー上のレコードの同じプロパティと比較されます。 要求で渡されたバージョンがバックエンドと一致しない場合、クライアント ライブラリは DatasyncConflictException<T> 例外を発生させます。 例外に含まれる型は、サーバー バージョンのレコードを含むバックエンドからのレコードです。 その後、アプリケーションはこの情報を使用して、バックエンドから正しい version 値を使用して更新要求を再実行して変更をコミットするかどうかを決定できます。

DatasyncClientData 基本オブジェクトを使用すると、オプティミスティック コンカレンシーが自動的に有効になります。

オプティミスティック コンカレンシーを有効にするだけでなく、コード内の DatasyncConflictException<T> 例外もキャッチする必要があります。 更新されたレコードに正しい version を適用して競合を解決し、解決されたレコードで呼び出しを繰り返します。 次のコードは、一度検出された書き込み競合を解決する方法を示しています。

private async void UpdateToDoItem(TodoItem item)
{
    DatasyncConflictException<TodoItem> exception = null;

    try
    {
        //update at the remote table
        await remoteTable.UpdateAsync(item);
    }
    catch (DatasyncConflictException<TodoItem> writeException)
    {
        exception = writeException;
    }

    if (exception != null)
    {
        // Conflict detected, the item has changed since the last query
        // Resolve the conflict between the local and server item
        await ResolveConflict(item, exception.Item);
    }
}


private async Task ResolveConflict(TodoItem localItem, TodoItem serverItem)
{
    //Ask user to choose the resolution between versions
    MessageDialog msgDialog = new MessageDialog(
        String.Format("Server Text: \"{0}\" \nLocal Text: \"{1}\"\n",
        serverItem.Text, localItem.Text),
        "CONFLICT DETECTED - Select a resolution:");

    UICommand localBtn = new UICommand("Commit Local Text");
    UICommand ServerBtn = new UICommand("Leave Server Text");
    msgDialog.Commands.Add(localBtn);
    msgDialog.Commands.Add(ServerBtn);

    localBtn.Invoked = async (IUICommand command) =>
    {
        // To resolve the conflict, update the version of the item being committed. Otherwise, you will keep
        // catching a MobileServicePreConditionFailedException.
        localItem.Version = serverItem.Version;

        // Updating recursively here just in case another change happened while the user was making a decision
        UpdateToDoItem(localItem);
    };

    ServerBtn.Invoked = async (IUICommand command) =>
    {
        RefreshTodoItems();
    };

    await msgDialog.ShowAsync();
}

オフライン テーブルの操作

オフライン テーブルでは、ローカル SQLite ストアを使用して、オフライン時に使用するデータを格納します。 すべてのテーブル操作は、リモート サーバー ストアではなくローカル SQLite ストアに対して実行されます。 各プラットフォーム プロジェクトと共有プロジェクトに Microsoft.Datasync.Client.SQLiteStore を追加します。

テーブル参照を作成する前に、ローカル ストアを準備する必要があります。

var store = new OfflineSQLiteStore(Constants.OfflineConnectionString);
store.DefineTable<TodoItem>();

ストアが定義されたら、クライアントを作成できます。

var options = new DatasyncClientOptions 
{
    OfflineStore = store
};
var client = new DatasyncClient("MOBILE_URL", options);

最後に、オフライン機能が初期化されていることを確認する必要があります。

await client.InitializeOfflineStoreAsync();

通常、ストアの初期化は、クライアントが作成された直後に行われます。 OfflineConnectionString は、SQLite データベースの場所と、データベースを開くために使用するオプションの両方を指定するために使用される URI です。 詳細については、「SQLiteの URI ファイル名を する」を参照してください。

  • メモリ内キャッシュを使用するには、file:inmemory.db?mode=memory&cache=privateを使用します。
  • ファイルを使用するには、file:/path/to/file.db

ファイルの絶対ファイル名を指定する必要があります。 Xamarin を使用している場合は、Xamarin Essentials ファイル システム ヘルパー を使用してパスを構築できます。次に例を示します。

var dbPath = $"{Filesystem.AppDataDirectory}/todoitems.db";
var store = new OfflineSQLiteStore($"file:/{dbPath}?mode=rwc");

MAUI を使用している場合は、MAUI ファイル システム ヘルパー を使用してパスを作成できます。次に例を示します。

var dbPath = $"{Filesystem.AppDataDirectory}/todoitems.db";
var store = new OfflineSQLiteStore($"file:/{dbPath}?mode=rwc");

オフライン テーブルを作成する

テーブル参照は、GetOfflineTable<T> メソッドを使用して取得できます。

IOfflineTable<TodoItem> table = client.GetOfflineTable<TodoItem>();

リモート テーブルと同様に、読み取り専用のオフライン テーブルを公開することもできます。

IReadOnlyOfflineTable<TodoItem> table = client.GetOfflineTable<TodoItem>();

オフライン テーブルを使用するために認証する必要はありません。 認証が必要なのは、バックエンド サービスと通信している場合のみです。

オフライン テーブルを同期する

オフライン テーブルは、既定ではバックエンドと同期されません。 同期は 2 つの部分に分割されます。 新しいアイテムのダウンロードとは別に変更をプッシュできます。 例えば:

public async Task SyncAsync()
{
    ReadOnlyCollection<TableOperationError> syncErrors = null;

    try
    {
        foreach (var offlineTable in offlineTables.Values)
        {
            await offlineTable.PushItemsAsync();
            await offlineTable.PullItemsAsync("", options);
        }
    }
    catch (PushFailedException exc)
    {
        if (exc.PushResult != null)
        {
            syncErrors = exc.PushResult.Errors;
        }
    }

    // Simple error/conflict handling
    if (syncErrors != null)
    {
        foreach (var error in syncErrors)
        {
            if (error.OperationKind == TableOperationKind.Update && error.Result != null)
            {
                //Update failed, reverting to server's copy.
                await error.CancelAndUpdateItemAsync(error.Result);
            }
            else
            {
                // Discard local change.
                await error.CancelAndDiscardItemAsync();
            }

            Debug.WriteLine(@"Error executing sync operation. Item: {0} ({1}). Operation discarded.", error.TableName, error.Item["id"]);
        }
    }
}

既定では、すべてのテーブルで増分同期が使用されます。新しいレコードのみが取得されます。 レコードは、一意のクエリごとに含まれます (OData クエリの MD5 ハッシュを作成することによって生成されます)。

手記

PullItemsAsync する最初の引数は、デバイスにプルするレコードを示す OData クエリです。 クライアント側で複雑なクエリを作成するのではなく、ユーザー固有のレコードのみを返すようにサービスを変更することをお勧めします。

(PullOptions オブジェクトによって定義される) オプションは、通常、設定する必要はありません。 オプションは次のとおりです。

  • PushOtherTables - true に設定すると、すべてのテーブルがプッシュされます。
  • QueryId - 生成されたクエリ ID ではなく、使用する特定のクエリ ID。
  • WriteDeltaTokenInterval - 増分同期の追跡に使用されるデルタ トークンを書き込む頻度。

SDK は、レコードをプルする前に暗黙的な PushAsync() を実行します。

競合処理は、PullAsync() メソッドで発生します。 オンライン テーブルと同じ方法で競合を処理します。 競合は、挿入、更新、または削除中ではなく、PullAsync() が呼び出されたときに生成されます。 複数の競合が発生した場合は、1 つの PushFailedExceptionにバンドルされます。 各エラーを個別に処理します。

すべてのテーブルの変更をプッシュする

すべての変更をリモート サーバーにプッシュするには、次のコマンドを使用します。

await client.PushTablesAsync();

テーブルのサブセットの変更をプッシュするには、PushTablesAsync() メソッドに IEnumerable<string> を指定します。

var tablesToPush = new string[] { "TodoItem", "Notes" };
await client.PushTables(tablesToPush);

client.PendingOperations プロパティを使用して、リモート サービスにプッシュされるのを待機している操作の数を読み取ります。 このプロパティは、オフライン ストアが構成されていない場合に null されます。

複雑な SQLite クエリを実行する

オフライン データベースに対して複雑な SQL クエリを実行する必要がある場合は、ExecuteQueryAsync() メソッドを使用して実行できます。 たとえば、SQL JOIN ステートメントを実行するには、戻り値の構造を示す JObject を定義してから、ExecuteQueryAsync()使用します。

var definition = new JObject() 
{
    { "id", string.Empty },
    { "title", string.Empty },
    { "first_name", string.Empty },
    { "last_name", string.Empty }
};
var sqlStatement = "SELECT b.id as id, b.title as title, a.first_name as first_name, a.last_name as last_name FROM books b INNER JOIN authors a ON b.author_id = a.id ORDER BY b.id";

var items = await store.ExecuteQueryAsync(definition, sqlStatement, parameters);
// Items is an IList<JObject> where each JObject conforms to the definition.

定義はキー/値のセットです。 キーは、SQL クエリが返すフィールド名と一致する必要があり、値は想定される型の既定値である必要があります。 数値 (long)、ブール値の false、その他のすべてに string.Empty には 0L を使用します。

SQLite には、サポートされている型の制限付きセットがあります。 日付/時刻は、比較を可能にするためにエポック以降のミリ秒数として格納されます。

ユーザーの認証

Azure Mobile Apps を使用すると、認証呼び出しを処理するための認証プロバイダーを生成できます。 サービス クライアントを構築するときに認証プロバイダーを指定します。

AuthenticationProvider authProvider = GetAuthenticationProvider();
var client = new DatasyncClient("APP_URL", authProvider);

認証が必要な場合は常に、トークンを取得するために認証プロバイダーが呼び出されます。 汎用認証プロバイダーは、承認ヘッダー ベースの認証と、App Service 認証と承認ベースの認証の両方に使用できます。 次のモデルを使用します。

public AuthenticationProvider GetAuthenticationProvider()
    => new GenericAuthenticationProvider(GetTokenAsync);

// Or, if using Azure App Service Authentication and Authorization
// public AuthenticationProvider GetAuthenticationProvider()
//    => new GenericAuthenticationProvider(GetTokenAsync, "X-ZUMO-AUTH");

public async Task<AuthenticationToken> GetTokenAsync()
{
    // TODO: Any code necessary to get the right access token.
    
    return new AuthenticationToken 
    {
        DisplayName = "/* the display name of the user */",
        ExpiresOn = DateTimeOffset.Now.AddHours(1), /* when does the token expire? */
        Token = "/* the access token */",
        UserId = "/* the user id of the connected user */"
    };
}

認証トークンはメモリにキャッシュされ (デバイスに書き込まれることはありません)、必要に応じて更新されます。

Microsoft ID プラットフォームを使用する

Microsoft ID プラットフォームを使用すると、Microsoft Entra ID と簡単に統合できます。 Microsoft Entra 認証を実装する方法の完全なチュートリアルについては、クイック スタート チュートリアルを参照してください。 次のコードは、アクセス トークンを取得する例を示しています。

private readonly string[] _scopes = { /* provide your AAD scopes */ };
private readonly object _parentWindow; /* Fill in with the required object before using */
private readonly PublicClientApplication _pca; /* Create one */

public MyAuthenticationHelper(object parentWindow) 
{
    _parentWindow = parentWindow;
    _pca = PublicClientApplicationBuilder.Create(clientId)
            .WithRedirectUri(redirectUri)
            .WithAuthority(authority)
            /* Add options methods here */
            .Build();
}

public async Task<AuthenticationToken> GetTokenAsync()
{
    // Silent authentication
    try
    {
        var account = await _pca.GetAccountsAsync().FirstOrDefault();
        var result = await _pca.AcquireTokenSilent(_scopes, account).ExecuteAsync();
        
        return new AuthenticationToken 
        {
            ExpiresOn = result.ExpiresOn,
            Token = result.AccessToken,
            UserId = result.Account?.Username ?? string.Empty
        };    
    }
    catch (Exception ex) when (exception is not MsalUiRequiredException)
    {
        // Handle authentication failure
        return null;
    }

    // UI-based authentication
    try
    {
        var account = await _pca.AcquireTokenInteractive(_scopes)
            .WithParentActivityOrWindow(_parentWindow)
            .ExecuteAsync();
        
        return new AuthenticationToken 
        {
            ExpiresOn = result.ExpiresOn,
            Token = result.AccessToken,
            UserId = result.Account?.Username ?? string.Empty
        };    
    }
    catch (Exception ex)
    {
        // Handle authentication failure
        return null;
    }
}

Microsoft ID プラットフォームと ASP.NET 6 の統合の詳細については、Microsoft ID プラットフォームの ドキュメントを参照してください。

Xamarin Essentials または MAUI WebAuthenticator を使用する

Azure App Service 認証の場合は、Xamarin Essentials WebAuthenticator または MAUI WebAuthenticator を使用してトークンを取得できます。

Uri authEndpoint = new Uri(client.Endpoint, "/.auth/login/aad");
Uri callback = new Uri("myapp://easyauth.callback");

public async Task<AuthenticationToken> GetTokenAsync()
{
    var authResult = await WebAuthenticator.AuthenticateAsync(authEndpoint, callback);
    return new AuthenticationToken 
    {
        ExpiresOn = authResult.ExpiresIn,
        Token = authResult.AccessToken
    };
}

Azure App Service 認証を使用する場合、UserIdDisplayName は直接使用できません。 代わりに、遅延リクエスタを使用して、/.auth/me エンドポイントから情報を取得します。

var userInfo = new AsyncLazy<UserInformation>(() => GetUserInformationAsync());

public async Task<UserInformation> GetUserInformationAsync() 
{
    // Get the token for the current user
    var authInfo = await GetTokenAsync();

    // Construct the request
    var request = new HttpRequestMessage(HttpMethod.Get, new Uri(client.Endpoint, "/.auth/me"));
    request.Headers.Add("X-ZUMO-AUTH", authInfo.Token);

    // Create a new HttpClient, then send the request
    var httpClient = new HttpClient();
    var response = await httpClient.SendAsync(request);

    // If the request is successful, deserialize the content into the UserInformation object.
    // You will have to create the UserInformation class.
    if (response.IsSuccessStatusCode) 
    {
        var content = await response.ReadAsStringAsync();
        return JsonSerializer.Deserialize<UserInformation>(content);
    }
}

高度なトピック

ローカル データベース内のエンティティの削除

通常の操作では、エンティティの削除は必要ありません。 同期プロセスでは、削除されたエンティティが削除され、ローカル データベース テーブルに必要なメタデータが保持されます。 ただし、データベース内のエンティティを削除すると便利な場合があります。 このようなシナリオの 1 つは、多数のエンティティを削除する必要があり、テーブルからデータをローカルでワイプする方が効率的な場合です。

テーブルからレコードを消去するには、table.PurgeItemsAsync()を使用します。

var query = table.CreateQuery();
var purgeOptions = new PurgeOptions();
await table.PurgeItermsAsync(query, purgeOptions, cancellationToken);

クエリは、テーブルから削除するエンティティを識別します。 LINQ を使用して消去するエンティティを特定します。

var query = table.CreateQuery().Where(m => m.Archived == true);

PurgeOptions クラスには、消去操作を変更するための設定が用意されています。

  • DiscardPendingOperations は、サーバーへの送信を待機している操作キュー内にあるテーブルの保留中の操作をすべて破棄します。
  • QueryId は、操作に使用するデルタ トークンを識別するために使用されるクエリ ID を指定します。
  • TimestampUpdatePolicy は、消去操作の終了時にデルタ トークンを調整する方法を指定します。
    • TimestampUpdatePolicy.NoUpdate は、デルタ トークンを更新してはならないと示します。
    • TimestampUpdatePolicy.UpdateToLastEntity は、デルタ トークンをテーブルに格納されている最後のエンティティの updatedAt フィールドに更新する必要があることを示します。
    • TimestampUpdatePolicy.UpdateToNow は、デルタ トークンを現在の日付/時刻に更新する必要があることを示します。
    • TimestampUpdatePolicy.UpdateToEpoch は、すべてのデータを同期するためにデルタ トークンをリセットする必要があることを示します。

table.PullItemsAsync() を呼び出してデータを同期するときに使用したのと同じ QueryId 値を使用します。 QueryId は、消去が完了したときに更新するデルタ トークンを指定します。

要求ヘッダーをカスタマイズする

特定のアプリ シナリオをサポートするには、モバイル アプリ バックエンドとの通信をカスタマイズすることが必要になる場合があります。 たとえば、ユーザーに戻る前に、すべての送信要求にカスタム ヘッダーを追加したり、応答状態コードを変更したりできます。 次の例のように、カスタム DelegatingHandlerを使用します。

public async Task CallClientWithHandler()
{
    var options = new DatasyncClientOptions
    {
        HttpPipeline = new DelegatingHandler[] { new MyHandler() }
    };
    var client = new Datasync("AppUrl", options);
    var todoTable = client.GetRemoteTable<TodoItem>();
    var newItem = new TodoItem { Text = "Hello world", Complete = false };
    await todoTable.InsertItemAsync(newItem);
}

public class MyHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // Change the request-side here based on the HttpRequestMessage
        request.Headers.Add("x-my-header", "my value");

        // Do the request
        var response = await base.SendAsync(request, cancellationToken);

        // Change the response-side here based on the HttpResponseMessage

        // Return the modified response
        return response;
    }
}

要求ログを有効にする

DelegatingHandler を使用して、要求ログを追加することもできます。

public class LoggingHandler : DelegatingHandler
{
    public LoggingHandler() : base() { }
    public LoggingHandler(HttpMessageHandler innerHandler) : base(innerHandler) { }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token)
    {
        Debug.WriteLine($"[HTTP] >>> {request.Method} {request.RequestUri}");
        if (request.Content != null)
        {
            Debug.WriteLine($"[HTTP] >>> {await request.Content.ReadAsStringAsync().ConfigureAwait(false)}");
        }

        HttpResponseMessage response = await base.SendAsync(request, token).ConfigureAwait(false);

        Debug.WriteLine($"[HTTP] <<< {response.StatusCode} {response.ReasonPhrase}");
        if (response.Content != null)
        {
            Debug.WriteLine($"[HTTP] <<< {await response.Content.ReadAsStringAsync().ConfigureAwait(false)}");
        }

        return response;
    }
}

同期イベントを監視する

同期イベントが発生すると、イベントは client.SynchronizationProgress イベント デリゲートに発行されます。 イベントを使用して、同期プロセスの進行状況を監視できます。 次のように同期イベント ハンドラーを定義します。

client.SynchronizationProgress += (sender, args) => {
    // args is of type SynchronizationEventArgs
};

SynchronizationEventArgs 型は次のように定義されます。

public enum SynchronizationEventType
{
    PushStarted,
    ItemWillBePushed,
    ItemWasPushed,
    PushFinished,
    PullStarted,
    ItemWillBeStored,
    ItemWasStored,
    PullFinished
}

public class SynchronizationEventArgs
{
    public SynchronizationEventType EventType { get; }
    public string ItemId { get; }
    public long ItemsProcessed { get; } 
    public long QueueLength { get; }
    public string TableName { get; }
    public bool IsSuccessful { get; }
}

args 内のプロパティは、プロパティが同期イベントに関連しない場合に null または -1 されます。