.NET 向け Azure Mobile Apps クライアント ライブラリの使用方法

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

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

Note

この記事では、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 および iOS for .NET)。
  • ユニバーサル Windows プラットフォーム ビルド 19041 以降。
  • Windows Presentation Foundation (WPF)。
  • Windows アプリ SDK (WinUI 3)。
  • Xamarin.Forms

さらに、AvaloniaUno Platform 向けのサンプルも作成されています。 TodoApp サンプルには、テスト済みの各プラットフォーム用の例が含まれています。

セットアップと前提条件

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

プラットフォーム プロジェクト (.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 が指定されていない場合は生成されます。 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");

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

サポートされている型

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

  • System.DateTime - ISO-8601 準拠の UTC 日付/時刻文字列 (ミリ秒精度) として。
  • System.DateTimeOffset - ISO-8601 準拠の UTC 日付/時刻文字列 (ミリ秒精度) として。
  • System.Guid - ハイフンで区切られた 32 桁の数字の形式。

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

リモート テーブルは、次のような LINQ 的なステートメントによって操作できます。

  • .Where() 句によるフィルター処理。
  • さまざまな .OrderBy() 句によるソート処理。
  • .Select() による プロパティの選択。
  • .Skip().Take() によるページング。

クエリから項目をカウントする

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

// 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")) でも使用できます。

Datasync Framework には、スレッドセーフで監視可能なコレクションである 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.FloorMath.Ceiling)
  • 文字列関数 (LengthSubstringReplaceIndexOfEqualsStartsWithEndsWith) (オーディナルおよびインバリアント カルチャのみ)、
  • 日付のプロパティ (YearMonthDayHourMinuteSecond)
  • オブジェクトのプロパティへのアクセス
  • これらの操作を組み合わせる式

データのソート処理

プロパティ アクセサー付きの .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 と共に前の例と同様のクエリを使用して、ページ間を移動することができます。

これまでに説明した関数はいずれも付加的なものであるため、これらの関数を連鎖させることができます。 連鎖した呼び出しはそれぞれ複数のクエリに影響を及ぼします。 次の例も参照してください。

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

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

挿入時に item に一意のカスタム ID 値が含まれていない場合は、サーバーによって 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);

競合の解決とオプティミスティック同時実行制御

複数のクライアントが同じ項目に対して同時に変更を書き込む場合があります。 競合を検出しない場合、最後に行われた書き込みによってそれ以前の更新がすべて上書きされます。 オプティミスティック同時実行制御では、それぞれのトランザクションがコミットでき、そのためリソース ロックが一切使用されないことを前提としています。 オプティミスティック同時実行制御では、トランザクションをコミットする前に、他のトランザクションがそのデータを変更していないことを確認します。 データが変更されている場合、トランザクションはロール バックされます。

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 ハッシュを作成することによって生成される) ごとに含まれています。

Note

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

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

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

レコードを取得する前に、SDK で暗黙的な PushAsync() が実行されます。

競合の処理は、PullAsync() メソッドで行われます。 競合の処理方法はオンライン テーブルの場合と同じです。 競合が発生するのは、挿入、更新、または削除のときではなく、PullAsync() が呼び出されたときです。 複数の競合が発生した場合、それらは単一の 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) には 0L を、ブール値には false を、それ以外には string.Empty を使用します。

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

ユーザーの認証

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

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

認証が必要な場合は常に、トークンを取得するために認証プロバイダーが呼び出されます。 汎用認証プロバイダーは、Authorization ヘッダー ベースの認証と、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 は、すべてのデータを同期するようにデルタ トークンをリセットする必要があることを示します。

QueryId は、table.PullItemsAsync() を呼び出してデータを同期したときに使用したものと同じ値を使用します。 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 のどちらかになります。