Bagikan melalui


Cara menggunakan pustaka klien Azure Mobile Apps untuk .NET

Nota

Produk ini dihentikan. Untuk pengganti proyek yang menggunakan .NET 8 atau yang lebih baru, lihat pustaka Community Toolkit Datasync.

Panduan ini menunjukkan kepada Anda cara melakukan skenario umum menggunakan pustaka klien .NET untuk Azure Mobile Apps. Gunakan pustaka klien .NET di aplikasi .NET 6 atau .NET Standard 2.0 apa pun, termasuk MAUI, Xamarin, dan Windows (WPF, UWP, dan WinUI).

Jika Anda baru menggunakan Azure Mobile Apps, pertimbangkan untuk terlebih dahulu menyelesaikan salah satu tutorial mulai cepat:

  • AvaloniaUI
  • MAUI (Android dan iOS)
  • Platform Uno
  • Windows (UWP)
  • Windows (WinUI3)
  • Windows (WPF)
  • Xamarin (Android Native)
  • Xamarin (iOS Native)
  • Xamarin Forms (Android dan iOS)

Nota

Artikel ini membahas edisi terbaru (v6.0) dari Microsoft Datasync Framework. Untuk klien yang lebih lama, lihat dokumentasi v4.2.0.

Platform yang didukung

Pustaka klien .NET mendukung platform .NET Standard 2.0 atau .NET 6 apa pun, termasuk:

  • .NET MAUI untuk platform Android, iOS, dan Windows.
  • Android API level 21 dan yang lebih baru (Xamarin dan Android untuk .NET).
  • iOS versi 12.0 dan yang lebih baru (Xamarin dan iOS untuk .NET).
  • Universal Windows Platform membangun 19041 dan yang lebih baru.
  • Windows Presentation Framework (WPF).
  • Windows App SDK (WinUI 3).
  • Xamarin.Forms

Selain itu, sampel telah dibuat untuk Avalonia dan Platform Uno. Sampel TodoApp berisi contoh setiap platform yang diuji.

Penyiapan dan Prasyarat

Tambahkan pustaka berikut dari NuGet:

  • Microsoft.Datasync.Client
  • Microsoft.Datasync.Client.SQLiteStore jika menggunakan tabel offline.

Jika menggunakan proyek platform (misalnya, .NET MAUI), pastikan Anda menambahkan pustaka ke proyek platform dan proyek bersama apa pun.

Membuat klien layanan

Kode berikut membuat klien layanan, yang digunakan untuk mengoordinasikan semua komunikasi ke tabel backend dan offline.

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

Dalam kode sebelumnya, ganti MOBILE_APP_URL dengan URL backend ASP.NET Core. Klien harus dibuat sebagai singleton. Jika menggunakan penyedia autentikasi, penyedia autentikasi dapat dikonfigurasi seperti ini:

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

Detail selengkapnya tentang penyedia autentikasi disediakan nanti dalam dokumen ini.

Pilihan

Sekumpulan opsi lengkap (default) dapat dibuat seperti ini:

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

Biasanya, permintaan HTTP dibuat dengan meneruskan permintaan melalui penyedia autentikasi (yang menambahkan header Authorization untuk pengguna yang saat ini diautentikasi) sebelum mengirim permintaan. Anda dapat, secara opsional, menambahkan lebih banyak penangan pendelegasian. Setiap permintaan melewati penangan pendelegasian sebelum dikirim ke layanan. Mendelegasikan handler memungkinkan Anda menambahkan header tambahan, melakukan percobaan ulang, atau menyediakan kemampuan pengelogan.

Contoh pendelegasian handler disediakan untuk pengelogan dan menambahkan header permintaan nanti di artikel ini.

IdGenerator

Saat entitas ditambahkan ke tabel offline, entitas harus memiliki ID. ID dihasilkan jika tidak disediakan. Opsi IdGenerator memungkinkan Anda menyesuaikan ID yang dihasilkan. Secara default, ID unik global dihasilkan. Misalnya, pengaturan berikut menghasilkan string yang menyertakan nama tabel dan GUID:

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

InstallationId

Jika InstallationId diatur, header kustom X-ZUMO-INSTALLATION-ID dikirim dengan setiap permintaan untuk mengidentifikasi kombinasi aplikasi pada perangkat tertentu. Header ini dapat direkam dalam log dan memungkinkan Anda menentukan jumlah penginstalan yang berbeda untuk aplikasi Anda. Jika Anda menggunakan InstallationId, ID harus disimpan dalam penyimpanan persisten pada perangkat sehingga penginstalan unik dapat dilacak.

OfflineStore

OfflineStore digunakan saat mengonfigurasi akses data offline. Untuk informasi selengkapnya, lihat Bekerja dengan tabel offline.

ParallelOperations

Bagian dari proses sinkronisasi offline melibatkan pendorongan operasi antrean ke server jarak jauh. Ketika operasi pendorongan dipicu, operasi dikirimkan dalam urutan yang diterima. Anda dapat, secara opsional, menggunakan hingga delapan utas untuk mendorong operasi ini. Operasi paralel menggunakan lebih banyak sumber daya pada klien dan server untuk menyelesaikan operasi lebih cepat. Urutan saat operasi tiba di server tidak dapat dijamin saat menggunakan beberapa utas.

SerializerSettings

Jika Anda telah mengubah pengaturan serializer di server sinkronisasi data, Anda perlu membuat perubahan yang sama pada SerializerSettings pada klien. Opsi ini memungkinkan Anda menentukan pengaturan serializer Anda sendiri.

TableEndpointResolver

Menurut konvensi, tabel terletak di layanan jarak jauh di jalur /tables/{tableName} (seperti yang ditentukan oleh atribut Route dalam kode server). Namun, tabel dapat ada di jalur titik akhir mana pun. TableEndpointResolver adalah fungsi yang mengubah nama tabel menjadi jalur untuk berkomunikasi dengan layanan jarak jauh.

Misalnya, berikut mengubah asumsi sehingga semua tabel terletak di bawah /api:

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

UserAgent

Klien sinkronisasi data menghasilkan nilai header User-Agent yang sesuai berdasarkan versi pustaka. Beberapa pengembang merasa header agen pengguna membocorkan informasi tentang klien. Anda dapat mengatur properti UserAgent ke nilai header yang valid.

Bekerja dengan tabel jarak jauh

Bagian berikut ini merinci cara mencari dan mengambil rekaman dan mengubah data dalam tabel jarak jauh. Topik berikut dibahas:

Membuat referensi tabel jarak jauh

Untuk membuat referensi tabel jarak jauh, gunakan GetRemoteTable<T>:

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

Jika Anda ingin mengembalikan tabel baca-saja, gunakan versi IReadOnlyRemoteTable<T>:

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

Jenis model harus menerapkan kontrak ITableData dari layanan. Gunakan DatasyncClientData untuk menyediakan bidang yang diperlukan:

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

Objek DatasyncClientData meliputi:

  • Id (string) - ID unik global untuk item.
  • UpdatedAt (System.DataTimeOffset) - tanggal/waktu item terakhir diperbarui.
  • Version (string) - string buram yang digunakan untuk penerapan versi.
  • Deleted (boolean) - jika true, item akan dihapus.

Layanan ini mempertahankan bidang-bidang ini. Jangan sesuaikan bidang ini sebagai bagian dari aplikasi klien Anda.

Model dapat diannotasi menggunakan atribut Newtonsoft.JSON. Nama tabel dapat ditentukan dengan menggunakan atribut DataTable:

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

Atau, tentukan nama tabel dalam panggilan GetRemoteTable():

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

Klien menggunakan jalur /tables/{tablename} sebagai URI. Nama tabel juga merupakan nama tabel offline dalam database SQLite.

Jenis yang didukung

Selain jenis primitif (int, float, string, dll.), jenis berikut ini didukung untuk model:

  • System.DateTime - sebagai string tanggal/waktu UTC ISO-8601 dengan akurasi ms.
  • System.DateTimeOffset - sebagai string tanggal/waktu UTC ISO-8601 dengan akurasi ms.
  • System.Guid - diformat sebagai 32 digit dipisahkan sebagai tanda hubung.

Mengkueri data dari server jarak jauh

Tabel jarak jauh dapat digunakan dengan pernyataan seperti LINQ, termasuk:

  • Pemfilteran dengan klausa .Where().
  • Mengurutkan dengan berbagai klausa .OrderBy().
  • Memilih properti dengan .Select().
  • Halaman dengan .Skip() dan .Take().

Menghitung item dari kueri

Jika Anda memerlukan hitungan item yang akan dikembalikan kueri, Anda bisa menggunakan .CountItemsAsync() pada tabel atau .LongCountAsync() pada kueri:

// 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();

Metode ini menyebabkan perjalanan pulang pergi ke server. Anda juga bisa mendapatkan hitungan saat mengisi daftar (misalnya), menghindari pulang-pergi tambahan:

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);
}

Jumlah akan diisi setelah permintaan pertama untuk mengambil konten tabel.

Mengembalikan semua data

Data dikembalikan melaluiIAsyncEnumerable :

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

Gunakan salah satu klausul penghentian berikut untuk mengonversi IAsyncEnumerable<T> ke koleksi yang berbeda:

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();

Di balik layar, tabel jarak jauh menangani halaman hasil untuk Anda. Semua item dikembalikan terlepas dari berapa banyak permintaan sisi server yang diperlukan untuk memenuhi kueri. Elemen-elemen ini juga tersedia pada hasil kueri (misalnya, remoteTable.Where(m => m.Rating == "R")).

Kerangka kerja Sinkronisasi data juga menyediakan ConcurrentObservableCollection<T> - koleksi yang dapat diamati dengan aman utas. Kelas ini dapat digunakan dalam konteks aplikasi UI yang biasanya menggunakan ObservableCollection<T> untuk mengelola daftar (misalnya, formulir Xamarin atau daftar MAUI). Anda dapat menghapus dan memuat ConcurrentObservableCollection<T> langsung dari tabel atau kueri:

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

Menggunakan .ToObservableCollection(collection) memicu peristiwa CollectionChanged sekali untuk seluruh koleksi daripada untuk item individual, menghasilkan waktu penguraian ulang yang lebih cepat.

ConcurrentObservableCollection<T> juga memiliki modifikasi berbasis predikat:

// 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);

Modifikasi berbasis predikat dapat digunakan dalam penanganan aktivitas saat indeks item tidak diketahui sebelumnya.

Memfilter data

Anda dapat menggunakan klausa .Where() untuk memfilter data. Misalnya:

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

Pemfilteran dilakukan pada layanan sebelum IAsyncEnumerable dan pada klien setelah IAsyncEnumerable. Misalnya:

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

Klausa .Where() pertama (hanya mengembalikan item yang tidak lengkap) dijalankan pada layanan, sedangkan klausa .Where() kedua (dimulai dengan "The") dijalankan pada klien.

Klausa Where mendukung operasi yang diterjemahkan ke dalam subset OData. Operasi meliputi:

  • Operator relasional (==, !=, <, <=, >, >=),
  • Operator aritmatika (+, -, /, *, %),
  • Presisi angka (Math.Floor, Math.Ceiling),
  • Fungsi string (Length, Substring, Replace, IndexOf, Equals, StartsWith, EndsWith) (hanya budaya ordinal dan invarian),
  • Properti tanggal (Year, Month, Day, Hour, Minute, Second),
  • Mengakses properti objek, dan
  • Ekspresi yang menggabungkan salah satu operasi ini.

Mengurutkan data

Gunakan .OrderBy(), .OrderByDescending(), .ThenBy(), dan .ThenByDescending() dengan aksesor properti untuk mengurutkan data.

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

Penyortiran dilakukan oleh layanan. Anda tidak dapat menentukan ekspresi dalam klausa pengurutan apa pun. Jika Anda ingin mengurutkan menurut ekspresi, gunakan pengurutan sisi klien:

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

Memilih properti

Anda dapat mengembalikan subkumpulan data dari layanan:

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

Mengembalikan halaman data

Anda dapat mengembalikan subset himpunan data menggunakan .Skip() dan .Take() untuk mengimplementasikan penomor:

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

Di aplikasi dunia nyata, Anda dapat menggunakan kueri yang mirip dengan contoh sebelumnya dengan kontrol pager atau antarmuka pengguna yang sebanding untuk menavigasi antar halaman.

Semua fungsi yang dijelaskan sejauh ini bersifat aditif, sehingga kita dapat terus menautkannya. Setiap panggilan berantai memengaruhi lebih banyak kueri. Satu contoh lagi:

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

Mencari data jarak jauh berdasarkan ID

Fungsi GetItemAsync dapat digunakan untuk mencari objek dari database dengan ID tertentu.

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

Jika item yang Anda coba ambil telah dihapus sementara, Anda harus menggunakan parameter 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);

Menyisipkan data di server jarak jauh

Semua jenis klien harus berisi anggota bernama Id, yang secara default merupakan string. Id ini diperlukan untuk melakukan operasi CRUD dan untuk sinkronisasi offline. Kode berikut mengilustrasikan cara menggunakan metode InsertItemAsync untuk menyisipkan baris baru ke dalam tabel. Parameter berisi data yang akan dimasukkan sebagai objek .NET.

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

Jika nilai ID kustom unik tidak disertakan dalam item selama penyisipan, server akan menghasilkan ID. Anda dapat mengambil ID yang dihasilkan dengan memeriksa objek setelah panggilan kembali.

Memperbarui data di server jarak jauh

Kode berikut ini menggambarkan cara menggunakan metode ReplaceItemAsync untuk memperbarui rekaman yang ada dengan ID yang sama dengan informasi baru.

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

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

Menghapus data di server jarak jauh

Kode berikut mengilustrasikan cara menggunakan metode DeleteItemAsync untuk menghapus instans yang ada.

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

await todoTable.DeleteItemAsync(item);

Resolusi konflik dan konkurensi optimis

Dua klien atau lebih dapat menulis perubahan pada item yang sama secara bersamaan. Tanpa deteksi konflik, tulisan terakhir akan menimpa pembaruan sebelumnya. Kontrol konkurensi optimis mengasumsikan bahwa setiap transaksi dapat diterapkan dan oleh karena itu tidak menggunakan penguncian sumber daya apa pun. Kontrol konkurensi optimis memverifikasi bahwa tidak ada transaksi lain yang telah memodifikasi data sebelum melakukan data. Jika data telah dimodifikasi, transaksi akan digulung balik.

Azure Mobile Apps mendukung kontrol konkurensi optimis dengan melacak perubahan pada setiap item menggunakan kolom properti sistem version yang ditentukan untuk setiap tabel di backend Mobile App Anda. Setiap kali rekaman diperbarui, Mobile Apps mengatur properti version untuk rekaman tersebut ke nilai baru. Selama setiap permintaan pembaruan, properti version catatan yang disertakan dengan permintaan dibandingkan dengan properti yang sama untuk rekaman di server. Jika versi yang diteruskan dengan permintaan tidak cocok dengan backend, maka pustaka klien akan memunculkan pengecualian DatasyncConflictException<T>. Jenis yang disertakan dengan pengecualian adalah rekaman dari backend yang berisi versi server rekaman. Aplikasi kemudian dapat menggunakan informasi ini untuk memutuskan apakah akan menjalankan permintaan pembaruan lagi dengan nilai version yang benar dari backend untuk menerapkan perubahan.

Konkurensi optimis diaktifkan secara otomatis saat menggunakan objek dasar DatasyncClientData.

Selain mengaktifkan konkurensi optimis, Anda juga harus menangkap pengecualian DatasyncConflictException<T> dalam kode Anda. Atasi konflik dengan menerapkan version yang benar ke rekaman yang diperbarui lalu ulangi panggilan dengan rekaman yang diselesaikan. Kode berikut menunjukkan cara mengatasi konflik tulis setelah terdeteksi:

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();
}

Bekerja dengan tabel offline

Tabel offline menggunakan penyimpanan SQLite lokal untuk menyimpan data untuk digunakan saat offline. Semua operasi tabel dilakukan terhadap penyimpanan SQLite lokal alih-alih penyimpanan server jarak jauh. Pastikan Anda menambahkan Microsoft.Datasync.Client.SQLiteStore ke setiap proyek platform dan ke proyek bersama apa pun.

Sebelum referensi tabel dapat dibuat, penyimpanan lokal harus disiapkan:

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

Setelah penyimpanan ditentukan, Anda dapat membuat klien:

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

Terakhir, Anda harus memastikan bahwa kemampuan offline diinisialisasi:

await client.InitializeOfflineStoreAsync();

Inisialisasi penyimpanan biasanya dilakukan segera setelah klien dibuat. OfflineConnectionString adalah URI yang digunakan untuk menentukan lokasi database SQLite dan opsi yang digunakan untuk membuka database. Untuk informasi selengkapnya, lihat Nama File URI di SQLite.

  • Untuk menggunakan cache dalam memori, gunakan file:inmemory.db?mode=memory&cache=private.
  • Untuk menggunakan file, gunakan file:/path/to/file.db

Anda harus menentukan nama file absolut untuk file tersebut. Jika menggunakan Xamarin, Anda dapat menggunakan Xamarin Essentials File System Helpers untuk membangun jalur: Misalnya:

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

Jika Anda menggunakan MAUI, Anda dapat menggunakan Maui File System Helpers untuk membuat jalur: Misalnya:

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

Membuat tabel offline

Referensi tabel dapat diperoleh menggunakan metode GetOfflineTable<T>:

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

Seperti halnya tabel jarak jauh, Anda juga dapat mengekspos tabel offline baca-saja:

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

Anda tidak perlu mengautentikasi untuk menggunakan tabel offline. Anda hanya perlu mengautentikasi saat berkomunikasi dengan layanan backend.

Menyinkronkan Tabel Offline

Tabel offline tidak disinkronkan dengan backend secara default. Sinkronisasi dibagi menjadi dua bagian. Anda dapat mendorong perubahan secara terpisah dari mengunduh item baru. Misalnya:

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"]);
        }
    }
}

Secara default, semua tabel menggunakan sinkronisasi bertambah bertahap - hanya rekaman baru yang diambil. Catatan disertakan untuk setiap kueri unik (dihasilkan dengan membuat hash MD5 dari kueri OData).

Nota

Argumen pertama untuk PullItemsAsync adalah kueri OData yang menunjukkan rekaman mana yang akan ditarik ke perangkat. Lebih baik memodifikasi layanan untuk hanya mengembalikan rekaman khusus untuk pengguna daripada membuat kueri kompleks di sisi klien.

Opsi (ditentukan oleh objek PullOptions) umumnya tidak perlu diatur. Opsinya meliputi:

  • PushOtherTables - jika diatur ke true, semua tabel akan didorong.
  • QueryId - ID kueri tertentu untuk digunakan daripada yang dihasilkan.
  • WriteDeltaTokenInterval - seberapa sering menulis delta-token yang digunakan untuk melacak sinkronisasi inkremental.

SDK melakukan PushAsync() implisit sebelum menarik rekaman.

Penanganan konflik terjadi pada metode PullAsync(). Tangani konflik dengan cara yang sama seperti tabel online. Konflik dihasilkan ketika PullAsync() dipanggil alih-alih selama penyisipan, pembaruan, atau penghapusan. Jika beberapa konflik terjadi, konflik tersebut dibundel ke dalam satu PushFailedException. Tangani setiap kegagalan secara terpisah.

Mendorong perubahan untuk semua tabel

Untuk mendorong semua perubahan ke server jarak jauh, gunakan:

await client.PushTablesAsync();

Untuk mendorong perubahan subkumpulan tabel, berikan IEnumerable<string> ke metode PushTablesAsync():

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

Gunakan properti client.PendingOperations untuk membaca jumlah operasi yang menunggu untuk didorong ke layanan jarak jauh. Properti ini null ketika tidak ada penyimpanan offline yang dikonfigurasi.

Menjalankan kueri SQLite yang kompleks

Jika Anda perlu melakukan kueri SQL kompleks terhadap database offline, Anda dapat melakukannya menggunakan metode ExecuteQueryAsync(). Misalnya, untuk melakukan pernyataan SQL JOIN, tentukan JObject yang menunjukkan struktur nilai pengembalian, lalu gunakan 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.

Definisinya adalah sekumpulan kunci/nilai. Kunci harus cocok dengan nama bidang yang dikembalikan kueri SQL, dan nilainya harus merupakan nilai default dari jenis yang diharapkan. Gunakan 0L untuk angka (panjang), false untuk boolean, dan string.Empty untuk yang lainnya.

SQLite memiliki sekumpulan jenis yang didukung secara ketat. Tanggal/waktu disimpan sebagai jumlah milidetik karena zaman untuk memungkinkan perbandingan.

Mengautentikasi pengguna

Azure Mobile Apps memungkinkan Anda membuat penyedia autentikasi untuk menangani panggilan autentikasi. Tentukan penyedia autentikasi saat membuat klien layanan:

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

Setiap kali autentikasi diperlukan, penyedia autentikasi dipanggil untuk mendapatkan token. Penyedia autentikasi generik dapat digunakan untuk autentikasi berbasis header Otorisasi dan Autentikasi App Service dan autentikasi berbasis Otorisasi. Gunakan model berikut:

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 */"
    };
}

Token autentikasi di-cache dalam memori (tidak pernah ditulis ke perangkat) dan disegarkan jika perlu.

Menggunakan platform identitas Microsoft

Platform identitas Microsoft memungkinkan Anda untuk dengan mudah berintegrasi dengan MICROSOFT Entra ID. Lihat tutorial mulai cepat untuk tutorial lengkap tentang cara menerapkan autentikasi Microsoft Entra. Kode berikut menunjukkan contoh pengambilan token akses:

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;
    }
}

Untuk informasi selengkapnya tentang mengintegrasikan platform identitas Microsoft dengan ASP.NET 6, lihat dokumentasi platform identitas Microsoft .

Menggunakan Xamarin Essentials atau MAUI WebAuthenticator

Untuk Autentikasi Azure App Service, Anda dapat menggunakan Xamarin Essentials WebAuthenticator atau MAUI WebAuthenticator untuk mendapatkan token:

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
    };
}

UserId dan DisplayName tidak tersedia secara langsung saat menggunakan Autentikasi Azure App Service. Sebagai gantinya, gunakan pemohon malas untuk mengambil informasi dari titik akhir /.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);
    }
}

Topik tingkat lanjut

Membersihkan entitas dalam database lokal

Dalam operasi normal, entitas pembersihan tidak diperlukan. Proses sinkronisasi menghapus entitas yang dihapus dan mempertahankan metadata yang diperlukan untuk tabel database lokal. Namun, ada kalanya membersihkan entitas dalam database sangat membantu. Salah satu skenario tersebut adalah ketika Anda perlu menghapus sejumlah besar entitas dan lebih efisien untuk menghapus data dari tabel secara lokal.

Untuk menghapus menyeluruh rekaman dari tabel, gunakan table.PurgeItemsAsync():

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

Kueri mengidentifikasi entitas yang akan dihapus dari tabel. Identifikasi entitas yang akan dibersihkan menggunakan LINQ:

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

Kelas PurgeOptions menyediakan pengaturan untuk mengubah operasi penghapusan menyeluruh:

  • DiscardPendingOperations membuang operasi yang tertunda untuk tabel yang berada dalam antrean operasi yang menunggu untuk dikirim ke server.
  • QueryId menentukan ID kueri yang digunakan untuk mengidentifikasi token delta yang akan digunakan untuk operasi.
  • TimestampUpdatePolicy menentukan cara menyesuaikan token delta di akhir operasi pembersihan:
    • TimestampUpdatePolicy.NoUpdate menunjukkan token delta tidak boleh diperbarui.
    • TimestampUpdatePolicy.UpdateToLastEntity menunjukkan token delta harus diperbarui ke bidang updatedAt untuk entitas terakhir yang disimpan dalam tabel.
    • TimestampUpdatePolicy.UpdateToNow menunjukkan token delta harus diperbarui ke tanggal/waktu saat ini.
    • TimestampUpdatePolicy.UpdateToEpoch menunjukkan token delta harus diatur ulang untuk menyinkronkan semua data.

Gunakan nilai QueryId yang sama dengan yang Anda gunakan saat memanggil table.PullItemsAsync() untuk menyinkronkan data. QueryId menentukan token delta untuk diperbarui saat pembersihan selesai.

Mengkustomisasi header permintaan

Untuk mendukung skenario aplikasi tertentu, Anda mungkin perlu menyesuaikan komunikasi dengan backend Aplikasi Seluler. Misalnya, Anda dapat menambahkan header kustom ke setiap permintaan keluar atau mengubah kode status respons sebelum kembali ke pengguna. Gunakan kustom DelegatingHandler, seperti dalam contoh berikut:

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;
    }
}

Aktifkan pengelogan permintaan

Anda juga dapat menggunakan DelegatingHandler untuk menambahkan pengelogan permintaan:

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;
    }
}

Memantau peristiwa sinkronisasi

Saat peristiwa sinkronisasi terjadi, peristiwa diterbitkan ke delegasi peristiwa client.SynchronizationProgress. Peristiwa dapat digunakan untuk memantau kemajuan proses sinkronisasi. Tentukan penanganan aktivitas sinkronisasi sebagai berikut:

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

Jenis SynchronizationEventArgs didefinisikan sebagai berikut:

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; }
}

Properti dalam argsnull atau -1 saat properti tidak relevan dengan peristiwa sinkronisasi.